Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
70ea5ccee6
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @jellyfin/roku
|
30
.github/release.yml
vendored
Normal file
30
.github/release.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
changelog:
|
||||
categories:
|
||||
- title: 🆕 New Features
|
||||
labels:
|
||||
- "new-feature"
|
||||
- title: ⚙️ New Settings
|
||||
labels:
|
||||
- "new-setting"
|
||||
- title: 🔧 General Improvements
|
||||
labels:
|
||||
- "general-improvement"
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- "bug-fix"
|
||||
- title: 🧹 Code Cleanup
|
||||
labels:
|
||||
- "code-cleanup"
|
||||
- title: 💻 Dev Improvements
|
||||
labels:
|
||||
- "dev-improvement"
|
||||
- title: 📝 Documentation
|
||||
labels:
|
||||
- "documentation"
|
||||
- title: ⭐ Additional Updates
|
||||
labels:
|
||||
- "*"
|
||||
exclude:
|
||||
labels:
|
||||
- dependencies
|
||||
- ignore-changelog
|
4
.github/workflows/auto-close-stale-pr.yml
vendored
4
.github/workflows/auto-close-stale-pr.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
|||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
@ -13,9 +14,10 @@ jobs:
|
|||
with:
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
stale-pr-label: stale
|
||||
stale-pr-message: "This pull request has been inactive for 21 days and will be automatically closed in 7 days if there is no further activity."
|
||||
close-pr-message: "This pull request has been closed because it has been inactive for 28 days. You may submit a new pull request if desired."
|
||||
days-before-pr-stale: 21
|
||||
days-before-pr-close: 7
|
||||
exempt-draft-pr: true
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
7
.github/workflows/automations.yml
vendored
7
.github/workflows/automations.yml
vendored
|
@ -12,6 +12,7 @@ on:
|
|||
|
||||
jobs:
|
||||
project:
|
||||
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||
name: Project board 📊
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -23,11 +24,13 @@ jobs:
|
|||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
label:
|
||||
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||
name: Labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check all PRs for merge conflicts ⛔
|
||||
uses: eps1lon/actions-label-merge-conflict@releases/2.x
|
||||
with:
|
||||
dirtyLabel: "merge conflict"
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
dirtyLabel: "merge-conflict"
|
||||
commentOnDirty: "This pull request has merge conflicts. Please resolve the conflicts so the PR can be reviewed. Thanks!"
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
21
.github/workflows/build-dev.yml
vendored
21
.github/workflows/build-dev.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: build
|
||||
name: build-dev
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -12,16 +12,19 @@ jobs:
|
|||
dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npx ropm install
|
||||
- run: make dev
|
||||
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
|
||||
- name: NPM install
|
||||
run: npm ci
|
||||
- name: Install roku module dependencies
|
||||
run: npm run ropm
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
with:
|
||||
name: Jellyfin-Roku-dev-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/out/staging
|
||||
if-no-files-found: error
|
||||
path: ${{ github.workspace }}/build/staging
|
||||
if-no-files-found: error
|
||||
|
|
33
.github/workflows/build-docs.yml
vendored
Normal file
33
.github/workflows/build-docs.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: build-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- unstable
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
- name: Install NPM dependencies
|
||||
run: npm ci
|
||||
- name: Build API docs
|
||||
# TODO: fix jsdoc build errors then remove '|| true' from run command below
|
||||
run: npm run docs || true
|
||||
- name: Commit any changes back to the unstable branch
|
||||
uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5
|
||||
with:
|
||||
commit_message: Update API docs
|
||||
# use jellyfin-bot to commit the changes instead of the default github-actions[bot]
|
||||
commit_user_name: jellyfin-bot
|
||||
commit_user_email: team@jellyfin.org
|
||||
# use jellyfin-bot to author the changes instead of the default author of the merge commit
|
||||
commit_author: jellyfin-bot <team@jellyfin.org>
|
29
.github/workflows/build-prod.yml
vendored
29
.github/workflows/build-prod.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: build
|
||||
name: build-prod
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master (the latest release)
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Install jq to parse json
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Save old Makefile version
|
||||
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Save new package.json version
|
||||
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: package.json version must be updated
|
||||
|
@ -51,7 +51,7 @@ jobs:
|
|||
if: env.oldManVersion == env.newManVersion
|
||||
run: exit 1
|
||||
- name: Save new Makefile version
|
||||
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Makefile version must be updated
|
||||
if: env.oldMakeVersion == env.newMakeVersion
|
||||
run: exit 1
|
||||
|
@ -61,16 +61,19 @@ jobs:
|
|||
prod:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npx ropm install
|
||||
- run: make release
|
||||
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
|
||||
- name: NPM install
|
||||
run: npm ci
|
||||
- name: Install roku module dependencies
|
||||
run: npm run ropm
|
||||
- name: Build app for production
|
||||
run: npm run build-prod
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
with:
|
||||
name: Jellyfin-Roku-release-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/out/staging
|
||||
if-no-files-found: error
|
||||
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/build/staging
|
||||
if-no-files-found: error
|
||||
|
|
43
.github/workflows/deploy-api-docs.yml
vendored
Normal file
43
.github/workflows/deploy-api-docs.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: deploy-api-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["unstable"]
|
||||
paths: ["docs/**"] # only run if the docs are updated
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2
|
||||
with:
|
||||
# Only upload the api docs folder
|
||||
path: "docs/api"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@77d7344265e1f960dab5c00dbff52287a70b0d4f # v3
|
39
.github/workflows/roku-analysis.yml
vendored
Normal file
39
.github/workflows/roku-analysis.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: roku-analysis
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
- name: NPM install
|
||||
run: npm ci
|
||||
- name: Install roku module dependencies
|
||||
run: npm run ropm
|
||||
- name: Build dev app
|
||||
if: env.BRANCH_NAME != 'master'
|
||||
run: npm run build
|
||||
- name: Build app for production
|
||||
if: env.BRANCH_NAME == 'master'
|
||||
run: npm run build-prod
|
||||
- name: Use Java 17
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
- name: Download the Static Channel Analysis CLI
|
||||
run: |
|
||||
curl -sSL "https://devtools.web.roku.com/static-channel-analysis/sca-cmd.zip" -o sca-cmd.zip
|
||||
unzip sca-cmd.zip
|
||||
- name: Run Roku Static Analysis
|
||||
run: ./sca-cmd/bin/sca-cmd ${{ github.workspace }}/build/staging --exit error
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,18 +1,17 @@
|
|||
*.svg
|
||||
jellyfin-roku.zip
|
||||
source/globals.brs
|
||||
.env
|
||||
|
||||
###BrightScript specific
|
||||
# BrightScript
|
||||
dist/apps
|
||||
out/
|
||||
|
||||
build/
|
||||
roku_modules
|
||||
|
||||
#NPM modules
|
||||
source/globals.brs
|
||||
jellyfin-roku.zip
|
||||
# Rooibos
|
||||
bsconfig-tdd.json
|
||||
# NPM
|
||||
node_modules/
|
||||
|
||||
#Eclipse
|
||||
# Eclipse
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
418
.vscode/brighterscript.code-snippets
vendored
Normal file
418
.vscode/brighterscript.code-snippets
vendored
Normal file
|
@ -0,0 +1,418 @@
|
|||
{
|
||||
"rooibos beforeEach": {
|
||||
"prefix": "beforeEach",
|
||||
"body": [
|
||||
"@beforeEach",
|
||||
"function ${2:namespace}_${3:itGroup}_beforeEach()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos afterEach": {
|
||||
"prefix": "afterEach",
|
||||
"body": [
|
||||
"@afterEach",
|
||||
"function ${2:namespace}_${3:itGroup}_afterEach()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos setup": {
|
||||
"prefix": "setup",
|
||||
"body": [
|
||||
"@setup",
|
||||
"function ${2:namespace}_setup()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos tearDown": {
|
||||
"prefix": "tearDown",
|
||||
"body": [
|
||||
"@tearDown",
|
||||
"function ${2:namespace}_tearDown()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos ignore": {
|
||||
"prefix": "ignore",
|
||||
"body": [
|
||||
"@ignore ${1:reason}",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos only": {
|
||||
"prefix": "only",
|
||||
"body": [
|
||||
"@only",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos testSuite": {
|
||||
"prefix": "suite",
|
||||
"body": [
|
||||
"@suite(\"$1\")",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos testcase": {
|
||||
"prefix": "it",
|
||||
"body": [
|
||||
"@it(\"$1\")",
|
||||
"function _()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos params": {
|
||||
"prefix": "params",
|
||||
"body": [
|
||||
"@params(${1:values})$0"
|
||||
]
|
||||
},
|
||||
"rooibos it": {
|
||||
"prefix": "describe",
|
||||
"body": [
|
||||
"'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++",
|
||||
"@describe(\"${1:groupName}\")",
|
||||
"'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++",
|
||||
"",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos stub": {
|
||||
"prefix": "stub",
|
||||
"body": [
|
||||
"m.stub(${1:target}, \"${2:methodName}\", [${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos mock": {
|
||||
"prefix": "expect",
|
||||
"body": [
|
||||
"${1:mockName} = m.mock(${2:target}, \"${3:methodName}\", ${4:expectedNumberOfcalls}, [${5:methodArgs}], ${6:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expect": {
|
||||
"prefix": "expect",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"${2:methodName}\", ${3:expectedNumberOfcalls}, [${4:methodArgs}], ${5:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectOnce": {
|
||||
"prefix": "expectOnce",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"${2:methodName}\", [${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectCallfunc": {
|
||||
"prefix": "expectCallfunc",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"callFunc\", [\"${2:methodName}\", ${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectObserveNodeField": {
|
||||
"prefix": "eonf",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"observeNodeField\", [${2:node},\"${3:fieldName}\", m.${4:callback}])",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectUnObserveNodeField": {
|
||||
"prefix": "eunf",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"unobserveNodeField\", [${2:node},\"${:fieldName}\", m.${4:callback}])",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectObjectOnce": {
|
||||
"prefix": "expectObjectOnce",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"${3:methodName}\", [${4:methodArgs}], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectGetInstance": {
|
||||
"prefix": "expectGetInstance",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"getInstance\", [\"${3:instanceName}\"], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectCreateSGNode": {
|
||||
"prefix": "expectCreateSGNode",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"createSGNode\", [\"${3:nodeType}\"$0], ${1:name})"
|
||||
]
|
||||
},
|
||||
"rooibos expectGetClassInstance": {
|
||||
"prefix": "expectGetClassInstance",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"getClassInstance\", [\"${3:instanceName}\"], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectExpectOnce": {
|
||||
"prefix": "expectExpect",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"${3:methodName}\", [${4:methodArgs}], ${1:name})",
|
||||
"m.expectOnce(${1:name}, \"${5:methodName}\", [${6:methodArgs}], ${7:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectNone": {
|
||||
"prefix": "expectNone",
|
||||
"body": [
|
||||
"m.expectNone(${1:target}, \"${2:methodName}\")",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertFalse": {
|
||||
"prefix": "assertFalse",
|
||||
"body": [
|
||||
"m.assertFalse(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAsync": {
|
||||
"prefix": "assertAsync",
|
||||
"body": [
|
||||
"m.AssertAsyncField(${1:value}, $2{:fieldName})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertTrue": {
|
||||
"prefix": "assertTrue",
|
||||
"body": [
|
||||
"m.assertTrue(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertEqual": {
|
||||
"prefix": "assertEqual",
|
||||
"body": [
|
||||
"m.assertEqual(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertLike": {
|
||||
"prefix": "assertLike",
|
||||
"body": [
|
||||
"m.assertLike(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotEqual": {
|
||||
"prefix": "assertNotEqual",
|
||||
"body": [
|
||||
"m.assertNotEqual(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertInvalid": {
|
||||
"prefix": "assertInvalid",
|
||||
"body": [
|
||||
"m.assertInvalid(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotInvalid": {
|
||||
"prefix": "assertNotInvalid",
|
||||
"body": [
|
||||
"m.assertNotInvalid(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAHasKey": {
|
||||
"prefix": "assertAAHasKey",
|
||||
"body": [
|
||||
"m.assertAAHasKey(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAANotHasKey": {
|
||||
"prefix": "assertAANotHasKey",
|
||||
"body": [
|
||||
"m.assertAANotHasKey(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAHasKeys": {
|
||||
"prefix": "assertAAHasKeys",
|
||||
"body": [
|
||||
"m.assertAAHasKeys(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAANotHasKeys": {
|
||||
"prefix": "assertAANotHasKeys",
|
||||
"body": [
|
||||
"m.assertAANotHasKeys(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContains": {
|
||||
"prefix": "assertArrayContains",
|
||||
"body": [
|
||||
"m.assertArrayContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotContains": {
|
||||
"prefix": "assertArrayNotContains",
|
||||
"body": [
|
||||
"m.assertArrayNotContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsSubset": {
|
||||
"prefix": "assertArrayContainsSubset",
|
||||
"body": [
|
||||
"m.assertArrayContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsAAs": {
|
||||
"prefix": "assertArrayContainsAAs",
|
||||
"body": [
|
||||
"m.assertArrayContainsAAs(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotContainsSubset": {
|
||||
"prefix": "assertArrayNotContainsSubset",
|
||||
"body": [
|
||||
"m.assertArrayNotContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayCount": {
|
||||
"prefix": "assertArrayCount",
|
||||
"body": [
|
||||
"m.assertArrayCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotCount": {
|
||||
"prefix": "assertArrayNotCount",
|
||||
"body": [
|
||||
"m.assertArrayNotCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertEmpty": {
|
||||
"prefix": "assertEmpty",
|
||||
"body": [
|
||||
"m.assertEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotEmpty": {
|
||||
"prefix": "assertNotEmpty",
|
||||
"body": [
|
||||
"m.assertNotEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsOnlyValuesOfType": {
|
||||
"prefix": "assertArrayContainsOnlyValuesOfType",
|
||||
"body": [
|
||||
"m.assertArrayContainsOnlyValuesOfType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertType": {
|
||||
"prefix": "assertType",
|
||||
"body": [
|
||||
"m.assertType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertSubType": {
|
||||
"prefix": "assertSubType",
|
||||
"body": [
|
||||
"m.assertSubType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeCount": {
|
||||
"prefix": "assertNodeCount",
|
||||
"body": [
|
||||
"m.assertNodeCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotCount": {
|
||||
"prefix": "assertNodeNotCount",
|
||||
"body": [
|
||||
"m.assertNodeNotCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeEmpty": {
|
||||
"prefix": "assertNodeEmpty",
|
||||
"body": [
|
||||
"m.assertNodeEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotEmpty": {
|
||||
"prefix": "assertNodeNotEmpty",
|
||||
"body": [
|
||||
"m.assertNodeNotEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeContains": {
|
||||
"prefix": "assertNodeContains",
|
||||
"body": [
|
||||
"m.assertNodeContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotContains": {
|
||||
"prefix": "assertNodeNotContains",
|
||||
"body": [
|
||||
"m.assertNodeNotContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeContainsFields": {
|
||||
"prefix": "assertNodeContainsFields",
|
||||
"body": [
|
||||
"m.assertNodeContainsFields(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotContainsFields": {
|
||||
"prefix": "assertNodeNotContainsFields",
|
||||
"body": [
|
||||
"m.assertNodeNotContainsFields(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAContainsSubset": {
|
||||
"prefix": "assertAAContainsSubset",
|
||||
"body": [
|
||||
"m.assertAAContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertMocks": {
|
||||
"prefix": "assertMocks",
|
||||
"body": [
|
||||
"m.assertMocks(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
}
|
||||
}
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -4,9 +4,10 @@
|
|||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"RokuCommunity.brightscript",
|
||||
"AliceBeckett.brightscriptcomment",
|
||||
"redhat.vscode-xml",
|
||||
"davidanson.vscode-markdownlint"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
}
|
||||
|
|
70
.vscode/launch.json
vendored
70
.vscode/launch.json
vendored
|
@ -4,7 +4,9 @@
|
|||
{
|
||||
"type": "brightscript",
|
||||
"request": "launch",
|
||||
"name": "Jellyfin Debug: Launch",
|
||||
"name": "Jellyfin Debug",
|
||||
"rootDir": "${workspaceFolder}/build/staging",
|
||||
"preLaunchTask": "build-dev",
|
||||
"stopOnEntry": false,
|
||||
// To enable RALE:
|
||||
// set "brightscript.debug.raleTrackerTaskFileLocation": "/absolute/path/to/rale/TrackerTask.xml" in your vscode user settings
|
||||
|
@ -14,14 +16,66 @@
|
|||
//"host": "${promptForHost}",
|
||||
//WARNING: don't edit this value. Instead, set "brightscript.debug.password": "YOUR_PASSWORD_HERE" in your vscode user settings
|
||||
//"password": "${promptForPassword}",
|
||||
},
|
||||
{
|
||||
"name": "Run tests",
|
||||
"type": "brightscript",
|
||||
"request": "launch",
|
||||
"consoleOutput": "full",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"preLaunchTask": "build-tests",
|
||||
"retainStagingFolder": true,
|
||||
"stopOnEntry": false,
|
||||
"files": [
|
||||
"components/**/*",
|
||||
"images/**/*",
|
||||
"locale/**/*",
|
||||
"settings/**/*",
|
||||
"source/**/*",
|
||||
"manifest"
|
||||
]
|
||||
"!**/images/*.*",
|
||||
"!**/fonts/*.*",
|
||||
"!*.jpg",
|
||||
"!*.png",
|
||||
"*",
|
||||
"*.*",
|
||||
"**/*.*",
|
||||
"!*.zip",
|
||||
"!**/*.zip"
|
||||
],
|
||||
"rootDir": "${workspaceFolder}/build",
|
||||
"sourceDirs": [
|
||||
"${workspaceFolder}/test-app"
|
||||
],
|
||||
"enableDebuggerAutoRecovery": true,
|
||||
"stopDebuggerOnAppExit": true,
|
||||
"enableVariablesPanel": false,
|
||||
"injectRaleTrackerTask": false,
|
||||
"enableDebugProtocol": false
|
||||
},
|
||||
{
|
||||
"name": "Run test-tdd",
|
||||
"type": "brightscript",
|
||||
"request": "launch",
|
||||
"consoleOutput": "full",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"preLaunchTask": "build-tdd",
|
||||
"retainStagingFolder": true,
|
||||
"stopOnEntry": false,
|
||||
"files": [
|
||||
"!**/images/*.*",
|
||||
"!**/fonts/*.*",
|
||||
"!*.jpg",
|
||||
"!*.png",
|
||||
"*",
|
||||
"*.*",
|
||||
"**/*.*",
|
||||
"!*.zip",
|
||||
"!**/*.zip"
|
||||
],
|
||||
"rootDir": "${workspaceFolder}/build",
|
||||
"sourceDirs": [
|
||||
"${workspaceFolder}/test-app"
|
||||
],
|
||||
"enableDebuggerAutoRecovery": true,
|
||||
"stopDebuggerOnAppExit": true,
|
||||
"enableVariablesPanel": false,
|
||||
"injectRaleTrackerTask": false,
|
||||
"enableDebugProtocol": false
|
||||
}
|
||||
]
|
||||
}
|
24
.vscode/settings.json
vendored
24
.vscode/settings.json
vendored
|
@ -1,7 +1,21 @@
|
|||
{
|
||||
"files.associations": {
|
||||
"*.ts": "xml"
|
||||
},
|
||||
"xml.format.maxLineWidth": 0,
|
||||
"editor.formatOnSave": true
|
||||
"files.associations": {
|
||||
"*.ts": "xml"
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-xml"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
},
|
||||
"xml.format.maxLineWidth": 0,
|
||||
"editor.formatOnSave": true,
|
||||
"brightscript.output.hyperlinkFormat": "FilenameAndFunction",
|
||||
"brightscript.bsdk": "node_modules/brighterscript",
|
||||
"search.exclude": {
|
||||
"**/.git": true,
|
||||
"**/node_modules": true,
|
||||
"docs/api/**": true
|
||||
},
|
||||
"brightscriptcomment.addExtraAtStartAndEnd": false
|
||||
}
|
77
.vscode/tasks.json
vendored
Normal file
77
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build-dev",
|
||||
"type": "shell",
|
||||
"command": "npm run build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-prod",
|
||||
"type": "shell",
|
||||
"command": "npm run build-prod",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-tests",
|
||||
"type": "shell",
|
||||
"command": "npm run build-tests",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-tdd",
|
||||
"type": "shell",
|
||||
"command": "npm run build-tdd",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
126
Makefile
126
Makefile
|
@ -1,28 +1,114 @@
|
|||
|
||||
#########################################################################
|
||||
# Makefile Usage:
|
||||
#
|
||||
# 1) Make sure that you have the curl command line executable in your path
|
||||
# 2) Set the variable ROKU_DEV_TARGET in your environment to the IP
|
||||
# address of your Roku box. (e.g. export ROKU_DEV_TARGET=192.168.1.1.
|
||||
# Set in your this variable in your shell startup (e.g. .bashrc)
|
||||
# 3) and set up the ROKU_DEV_PASSWORD environment variable, too
|
||||
##########################################################################
|
||||
# Need curl and npm in your $PATH
|
||||
# If you want to get_images, you'll also need convert from ImageMagick
|
||||
##########################################################################
|
||||
|
||||
APPNAME = Jellyfin_Roku
|
||||
VERSION = 1.6.6
|
||||
VERSION := 2.0.0
|
||||
|
||||
ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
|
||||
## usage
|
||||
|
||||
include app.mk
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "targets"
|
||||
@echo " build-dev build development package"
|
||||
@echo " build-prod build production package"
|
||||
@echo " build-tests build tests package"
|
||||
@echo " format format brighscripts"
|
||||
@echo " lint lint code and documentation"
|
||||
@echo " get_images update official jellyfin images"
|
||||
@echo "targets needing ROKU_DEV_TARGET"
|
||||
@echo " home press the home button on device"
|
||||
@echo " launch launch installed"
|
||||
@echo "targets needing ROKU_DEV_TARGET and ROKU_DEV_PASSWORD"
|
||||
@echo " install install on device"
|
||||
@echo " remove remove installed from device"
|
||||
@echo " screenshot take a screenshot"
|
||||
@echo " deploy lint, remove, install"
|
||||
@echo "environment"
|
||||
@echo " ROKU_DEV_TARGET with device's IP"
|
||||
@echo " ROKU_DEV_PASSWORD with device's password"
|
||||
|
||||
dev:
|
||||
$(MAKE) BUILD='dev' package
|
||||
## development
|
||||
|
||||
beta:
|
||||
$(MAKE) BUILD='beta' package
|
||||
BUILT_PKG := out/$(notdir $(CURDIR)).zip
|
||||
|
||||
release:
|
||||
$(MAKE) BUILD='release' package
|
||||
node_modules/: package-lock.json; npm ci
|
||||
|
||||
deploy: prep_staging remove install
|
||||
.PHONY: build-dev build-prod build-tests
|
||||
.NOTPARALLEL: build-dev build-prod build-tests # output to the same file
|
||||
build-dev: node_modules/; npm run build
|
||||
build-prod: node_modules/; npm run build-prod
|
||||
build-tests: node_modules/; npm run build-tests
|
||||
|
||||
# default to build-dev if file doesn't exist
|
||||
$(BUILT_PKG):; $(MAKE) build-dev
|
||||
|
||||
.PHONY: format
|
||||
format: node_modules/; npm run format
|
||||
|
||||
.PHONY: lint
|
||||
lint: node_modules/; npm run lint
|
||||
|
||||
## roku box
|
||||
|
||||
CURL_CMD ?= curl --show-error
|
||||
|
||||
ifdef ROKU_DEV_TARGET
|
||||
|
||||
.PHONY: home launch
|
||||
home:
|
||||
$(CURL_CMD) -XPOST http://$(ROKU_DEV_TARGET):8060/keypress/home
|
||||
sleep 2 # wait for device reaction
|
||||
launch:
|
||||
$(CURL_CMD) -XPOST http://$(ROKU_DEV_TARGET):8060/launch/dev
|
||||
|
||||
ifdef ROKU_DEV_PASSWORD
|
||||
|
||||
CURL_LOGGED_CMD := $(CURL_CMD) --user rokudev:$(ROKU_DEV_PASSWORD) --digest
|
||||
|
||||
EXTRACT_ERROR_CMD := grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[["
|
||||
.PHONY: install remove
|
||||
install: $(BUILT_PKG) home
|
||||
$(CURL_LOGGED_CMD) -F "mysubmit=Install" -F "archive=@$<" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | $(EXTRACT_ERROR_CMD)
|
||||
$(MAKE) launch
|
||||
remove:
|
||||
$(CURL_LOGGED_CMD) -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | $(EXTRACT_ERROR_CMD)
|
||||
|
||||
.PHONY: screenshot
|
||||
screenshot:
|
||||
$(CURL_LOGGED_CMD) -o screenshot.jpg "http://$(ROKU_DEV_TARGET)/pkgs/dev.jpg"
|
||||
|
||||
.PHONY: deploy
|
||||
.NOTPARALLEL: deploy
|
||||
deploy: lint remove install
|
||||
|
||||
endif # ROKU_DEV_PASSWORD
|
||||
|
||||
endif # ROKU_DEV_TARGET
|
||||
|
||||
## sync branding
|
||||
|
||||
CONVERT_CMD ?= convert -gravity center
|
||||
CONVERT_BLUEBG_CMD := $(CONVERT_CMD) -background "\#000b25"
|
||||
BANNER := images/banner-dark.svg
|
||||
ICON := images/icon-transparent.svg
|
||||
|
||||
images/:; mkdir $@
|
||||
|
||||
.PHONY: redo # force rerun
|
||||
$(BANNER) $(ICON): images/ redo
|
||||
$(CURL_CMD) https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/$(@F) > $@
|
||||
|
||||
images/logo.png: $(BANNER); $(CONVERT_CMD) -background none -scale 1000x48 -extent 180x48 $< $@
|
||||
images/channel-poster_fhd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 535x400 -extent 540x405 $< $@
|
||||
images/channel-poster_hd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 275x205 -extent 336x210 $< $@
|
||||
images/channel-poster_sd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 182x135 -extent 246x140 $< $@
|
||||
images/splash-screen_fhd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 540x540 -extent 1920x1080 $< $@
|
||||
images/splash-screen_hd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 360x360 -extent 1280x720 $< $@
|
||||
images/splash-screen_sd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 240x240 -extent 720x480 $< $@
|
||||
|
||||
.PHONY: get_images
|
||||
get_images: $(ICON)
|
||||
get_images: images/logo.png
|
||||
get_images: images/channel-poster_fhd.png images/channel-poster_hd.png images/channel-poster_sd.png
|
||||
get_images: images/splash-screen_fhd.jpg images/splash-screen_hd.jpg images/splash-screen_sd.jpg
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
[![Logo Banner](https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true "Jellyfin")](https://jellyfin.org)
|
||||
|
||||
[![Code Documentation](https://img.shields.io/badge/Code%20Documentation-purple)](https://jellyfin.github.io/jellyfin-roku/)
|
||||
[![Build Status](https://img.shields.io/github/actions/workflow/status/jellyfin/jellyfin-roku/build-dev.yml?logo=github&branch=unstable "Build Status")](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable)
|
||||
[![Current Release](https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg?logo=github "Current Release")](https://github.com/jellyfin/jellyfin-roku/releases)
|
||||
[![Translation Status](https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg "Translation Status")](https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget)
|
||||
[![Forum](https://img.shields.io/badge/forum-MyBB-00A4DC "Check out our forum!")](https://forum.jellyfin.org/f-roku-development)
|
||||
[![Matrix](https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix "Chat on Matrix")](https://matrix.to/#/#jellyfin-dev-roku:matrix.org)
|
||||
[![Reddit](https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg?logo=reddit "Join our Subreddit")](https://www.reddit.com/r/jellyfin)
|
||||
[![License](https://img.shields.io/github/license/jellyfin/jellyfin-roku.svg "GPL 2.0 License")](LICENSE)
|
||||
|
||||
Jellyfin Roku is the official Jellyfin client for Roku devices. We welcome all contributions and pull requests! If you have a larger feature in mind please [open an issue](https://github.com/jellyfin/jellyfin-roku/issues/new?assignees=&labels=feature&template=feature_request.md&title=) so we can discuss the implementation before you start.
|
||||
|
@ -34,7 +35,7 @@ To test the latest features before they get released:
|
|||
|
||||
## Advanced
|
||||
|
||||
For more advanced deployment methods, access to crash logs, or to learn how to setup a developer environment so you can write some code yourself please read the [DEVGUIDE](DEVGUIDE.md).
|
||||
For more advanced deployment methods, access to crash logs, or to learn how to setup a developer environment so you can write some code yourself please read the [DEVGUIDE](docs/DEVGUIDE.md).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
|
|
215
app.mk
215
app.mk
|
@ -1,215 +0,0 @@
|
|||
#########################################################################
|
||||
# common include file for application Makefiles
|
||||
#
|
||||
# Makefile Common Usage:
|
||||
# > make
|
||||
# > make install
|
||||
# > make remove
|
||||
#
|
||||
# By default, ZIP_EXCLUDE will exclude -x \*.pkg -x storeassets\* -x keys\* -x .\*
|
||||
# If you define ZIP_EXCLUDE in your Makefile, it will override the default setting.
|
||||
#
|
||||
# To exclude different files from being added to the zipfile during packaging
|
||||
# include a line like this:ZIP_EXCLUDE= -x keys\*
|
||||
# that will exclude any file who's name begins with 'keys'
|
||||
# to exclude using more than one pattern use additional '-x <pattern>' arguments
|
||||
# ZIP_EXCLUDE= -x \*.pkg -x storeassets\*
|
||||
#
|
||||
# Important Notes:
|
||||
# To use the "install" and "remove" targets to install your
|
||||
# application directly from the shell, you must do the following:
|
||||
#
|
||||
# 1) Make sure that you have the curl command line executable in your path
|
||||
# 2) Set the variable ROKU_DEV_TARGET in your environment to the IP
|
||||
# address of your Roku box. (e.g. export ROKU_DEV_TARGET=192.168.1.1.
|
||||
# Set in your this variable in your shell startup (e.g. .bashrc)
|
||||
# 3) Set the variable ROKU_DEV_PASSWORD in your environment for the password
|
||||
# associated with the rokudev account.
|
||||
##########################################################################
|
||||
|
||||
BUILD = dev
|
||||
|
||||
DISTREL = $(shell pwd)/out
|
||||
COMMONREL ?= $(shell pwd)/common
|
||||
SOURCEREL = $(shell pwd)
|
||||
|
||||
ZIPREL = $(DISTREL)/apps
|
||||
STAGINGREL = $(DISTREL)/staging
|
||||
PKGREL = $(DISTREL)/packages
|
||||
|
||||
APPSOURCEDIR = source
|
||||
IMPORTFILES = $(foreach f,$(IMPORTS),$(COMMONREL)/$f.brs)
|
||||
IMPORTCLEANUP = $(foreach f,$(IMPORTS),$(APPSOURCEDIR)/$f.brs)
|
||||
|
||||
GITCOMMIT = $(shell git rev-parse --short HEAD)
|
||||
BUILDDATE = $(shell date -u | awk '{ print $$2,$$3,$$6,$$4 }')
|
||||
|
||||
BRANDING_ROOT = https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG
|
||||
ICON_SOURCE = icon-transparent.svg
|
||||
BANNER_SOURCE = banner-dark.svg
|
||||
OUTPUT_DIR = ./images
|
||||
|
||||
# Locales supported by Roku
|
||||
SUPPORTED_LOCALES = en_US en_GB fr_CA es_ES de_DE it_IT pt_BR
|
||||
|
||||
ifdef ROKU_DEV_PASSWORD
|
||||
USERPASS = rokudev:$(ROKU_DEV_PASSWORD)
|
||||
else
|
||||
USERPASS = rokudev
|
||||
endif
|
||||
|
||||
ifndef ZIP_EXCLUDE
|
||||
ZIP_EXCLUDE= -x \*.pkg -x storeassets\* -x keys\* -x \*/.\*
|
||||
endif
|
||||
|
||||
HTTPSTATUS = $(shell curl --silent --write-out "\n%{http_code}\n" $(ROKU_DEV_TARGET))
|
||||
|
||||
ifeq "$(HTTPSTATUS)" " 401"
|
||||
CURLCMD = curl -S --tcp-fastopen --connect-timeout 2 --max-time 30 --retry 5
|
||||
else
|
||||
CURLCMD = curl -S --tcp-fastopen --connect-timeout 2 --max-time 30 --retry 5 --user $(USERPASS) --digest
|
||||
endif
|
||||
|
||||
home:
|
||||
@echo "Forcing roku to main menu screen $(ROKU_DEV_TARGET)..."
|
||||
curl -s -S -d '' http://$(ROKU_DEV_TARGET):8060/keypress/home
|
||||
sleep 2
|
||||
|
||||
prep_staging:
|
||||
@echo "*** Preparing Staging Area ***"
|
||||
@echo " >> removing old application zip $(ZIPREL)/$(APPNAME).zip"
|
||||
@if [ -e "$(ZIPREL)/$(APPNAME).zip" ]; \
|
||||
then \
|
||||
rm $(ZIPREL)/$(APPNAME).zip; \
|
||||
fi
|
||||
|
||||
@echo " >> creating destination directory $(ZIPREL)"
|
||||
@if [ ! -d $(ZIPREL) ]; \
|
||||
then \
|
||||
mkdir -p $(ZIPREL); \
|
||||
fi
|
||||
|
||||
@echo " >> setting directory permissions for $(ZIPREL)"
|
||||
@if [ ! -w $(ZIPREL) ]; \
|
||||
then \
|
||||
chmod 755 $(ZIPREL); \
|
||||
fi
|
||||
|
||||
@echo " >> creating destination directory $(STAGINGREL)"
|
||||
@if [ -d $(STAGINGREL) ]; \
|
||||
then \
|
||||
find $(STAGINGREL) -delete; \
|
||||
fi; \
|
||||
mkdir -p $(STAGINGREL); \
|
||||
chmod -R 755 $(STAGINGREL); \
|
||||
|
||||
echo " >> moving application to $(STAGINGREL)"
|
||||
cp $(SOURCEREL)/manifest $(STAGINGREL)/manifest
|
||||
cp -r $(SOURCEREL)/source $(STAGINGREL)
|
||||
cp -r $(SOURCEREL)/components $(STAGINGREL)
|
||||
cp -r $(SOURCEREL)/images $(STAGINGREL)
|
||||
cp -r $(SOURCEREL)/settings $(STAGINGREL)
|
||||
|
||||
# Copy only supported languages over to staging
|
||||
mkdir $(STAGINGREL)/locale
|
||||
cp -r $(foreach f,$(SUPPORTED_LOCALES),$(SOURCEREL)/locale/$f) $(STAGINGREL)/locale
|
||||
|
||||
ifneq ($(BUILD), dev)
|
||||
echo "COPYING $(BUILD)"
|
||||
cp $(SOURCEREL)/resources/branding/$(BUILD)/* $(STAGINGREL)/images
|
||||
endif
|
||||
|
||||
package: prep_staging
|
||||
@echo "*** Creating $(APPNAME).zip ***"
|
||||
@echo " >> copying imports"
|
||||
@if [ "$(IMPORTFILES)" ]; \
|
||||
then \
|
||||
mkdir $(APPSOURCEDIR)/common; \
|
||||
cp -f -p -v $(IMPORTFILES) $(APPSOURCEDIR)/common/; \
|
||||
fi \
|
||||
|
||||
@echo " >> generating build info file"
|
||||
mkdir -p $(STAGINGREL)/$(APPSOURCEDIR)
|
||||
@if [ -e "$(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs" ]; \
|
||||
then \
|
||||
rm $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs; \
|
||||
fi
|
||||
echo " >> generating build info file";\
|
||||
echo "Function BuildDate()" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
echo " return \"${BUILDDATE}\"" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
echo "End Function" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
echo "Function BuildCommit()" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
echo " return \"${GITCOMMIT}\"" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
echo "End Function" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
||||
|
||||
# zip .png files without compression
|
||||
# do not zip up any files ending with '~'
|
||||
@echo " >> creating application zip $(STAGINGREL)/../apps/$(APPNAME)-$(BUILD).zip"
|
||||
@if [ -d $(STAGINGREL) ]; \
|
||||
then \
|
||||
cd $(STAGINGREL); \
|
||||
(zip -0 -r "../apps/$(APPNAME)-$(BUILD).zip" . -i \*.png $(ZIP_EXCLUDE)); \
|
||||
(zip -9 -r "../apps/$(APPNAME)-$(BUILD).zip" . -x \*~ -x \*.png $(ZIP_EXCLUDE)); \
|
||||
cd $(SOURCEREL);\
|
||||
else \
|
||||
echo "Source for $(APPNAME) not found at $(STAGINGREL)"; \
|
||||
fi
|
||||
|
||||
@if [ "$(IMPORTCLEANUP)" ]; \
|
||||
then \
|
||||
echo " >> deleting imports";\
|
||||
rm -r -f $(APPSOURCEDIR)/common; \
|
||||
fi \
|
||||
|
||||
@echo "*** packaging $(APPNAME)-$(BUILD) complete ***"
|
||||
|
||||
prep_commit:
|
||||
npm run format
|
||||
npm ci
|
||||
npm run validate
|
||||
npm run check-formatting
|
||||
|
||||
install: prep_staging package home
|
||||
@echo "Installing $(APPNAME)-$(BUILD) to host $(ROKU_DEV_TARGET)"
|
||||
@$(CURLCMD) --user $(USERPASS) --digest -F "mysubmit=Install" -F "archive=@$(ZIPREL)/$(APPNAME)-$(BUILD).zip" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[["
|
||||
|
||||
remove:
|
||||
@echo "Removing $(APPNAME) from host $(ROKU_DEV_TARGET)"
|
||||
@if [ "$(HTTPSTATUS)" == " 401" ]; \
|
||||
then \
|
||||
$(CURLCMD) --user $(USERPASS) --digest -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[[" ; \
|
||||
else \
|
||||
curl -s -S -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[[" ; \
|
||||
fi
|
||||
|
||||
get_images:
|
||||
@if [ ! -d $(OUTPUT_DIR) ]; \
|
||||
then \
|
||||
mkdir -p $(OUTPUT_DIR); \
|
||||
echo "Creating images folder"; \
|
||||
fi
|
||||
|
||||
echo "Downloading SVG source files from $(BRANDING_ROOT)"
|
||||
@wget $(BRANDING_ROOT)/$(ICON_SOURCE) > /dev/null
|
||||
@wget $(BRANDING_ROOT)/$(BANNER_SOURCE) > /dev/null
|
||||
echo "Finished downloading SVG files"
|
||||
|
||||
echo "Creating image files"
|
||||
@convert -background "#000b25" -gravity center -scale 535x400 -extent 540x405 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_fhd.png
|
||||
@convert -background "#000b25" -gravity center -scale 275x205 -extent 336x210 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_hd.png
|
||||
@convert -background "#000b25" -gravity center -scale 182x135 -extent 246x140 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_sd.png
|
||||
|
||||
@convert -background none -gravity center -scale 1000x48 -extent 180x48 $(BANNER_SOURCE) $(OUTPUT_DIR)/logo.png
|
||||
|
||||
@convert -background "#000b25" -gravity center -scale 540x540 -extent 1920x1080 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_fhd.jpg
|
||||
@convert -background "#000b25" -gravity center -scale 360x360 -extent 1280x720 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_hd.jpg
|
||||
@convert -background "#000b25" -gravity center -scale 240x240 -extent 720x480 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_sd.jpg
|
||||
echo "Finished creating image files"
|
||||
|
||||
screenshot:
|
||||
SCREENSHOT_TIME=`date "+%s"`; \
|
||||
curl -m 1 -o screenshot.jpg --user $(USERPASS) --digest "http://$(ROKU_DEV_TARGET)/pkgs/dev.jpg?time=$$SCREENSHOT_TIME" -H 'Accept: image/png,image/*;q=0.8,*/*;q=0.5' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate'
|
||||
|
||||
|
||||
|
||||
|
30
bsconfig-prod.json
Normal file
30
bsconfig-prod.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"files": [
|
||||
"manifest",
|
||||
"source/**/*.*",
|
||||
"components/**/*.*",
|
||||
"images/**/*.*",
|
||||
"locale/en_US/translations.ts",
|
||||
"locale/en_GB/translations.ts",
|
||||
"locale/fr_CA/translations.ts",
|
||||
"locale/es_ES/translations.ts",
|
||||
"locale/de_DE/translations.ts",
|
||||
"locale/it_IT/translations.ts",
|
||||
"locale/pt_BR/translations.ts",
|
||||
"settings/**/*.*",
|
||||
{
|
||||
"src": "resources/branding/release/*.*",
|
||||
"dest": "images"
|
||||
}
|
||||
],
|
||||
"plugins": ["@rokucommunity/bslint", "roku-log-bsc-plugin"],
|
||||
"rokuLog": {
|
||||
"strip": true,
|
||||
"insertPkgPath": false,
|
||||
"removeComments": true
|
||||
},
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"autoImportComponentScript": true,
|
||||
"stagingDir": "build/staging",
|
||||
"retainStagingDir": true
|
||||
}
|
48
bsconfig-tdd-sample.json
Normal file
48
bsconfig-tdd-sample.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"files": [
|
||||
{
|
||||
"src": "test-app/**/*"
|
||||
},
|
||||
{
|
||||
"src": "source/**/!(Main.brs)",
|
||||
"dest": "source"
|
||||
},
|
||||
{
|
||||
"src": "components/**/*",
|
||||
"dest": "components"
|
||||
},
|
||||
{
|
||||
"src": "locale/**/*",
|
||||
"dest": "locale"
|
||||
},
|
||||
{
|
||||
"src": "settings/**/*",
|
||||
"dest": "settings"
|
||||
},
|
||||
"!**/*.spec.bs",
|
||||
{
|
||||
"src": "**/BaseTestSuite.spec.bs",
|
||||
"dest": "source"
|
||||
},
|
||||
{
|
||||
"src": "**/isValid.spec.bs",
|
||||
"dest": "source"
|
||||
}
|
||||
],
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"autoImportComponentScript": true,
|
||||
"allowBrighterScriptInBrightScript": true,
|
||||
"createPackage": false,
|
||||
"stagingFolderPath": "build",
|
||||
"plugins": ["rooibos-roku"],
|
||||
"rooibos": {
|
||||
"isRecordingCodeCoverage": false,
|
||||
"testsFilePattern": null,
|
||||
"showOnlyFailures": true,
|
||||
"catchCrashes": true,
|
||||
"lineWidth": 70,
|
||||
"failFast": false,
|
||||
"sendHomeOnFinish": false
|
||||
},
|
||||
"sourceMap": true
|
||||
}
|
38
bsconfig-tests.json
Normal file
38
bsconfig-tests.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"files": [
|
||||
{
|
||||
"src": "test-app/**/*"
|
||||
},
|
||||
{
|
||||
"src": "source/**/!(Main.brs)",
|
||||
"dest": "source"
|
||||
},
|
||||
{
|
||||
"src": "components/**/*",
|
||||
"dest": "components"
|
||||
},
|
||||
{
|
||||
"src": "locale/**/*",
|
||||
"dest": "locale"
|
||||
},
|
||||
{
|
||||
"src": "settings/**/*",
|
||||
"dest": "settings"
|
||||
}
|
||||
],
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"autoImportComponentScript": true,
|
||||
"allowBrighterScriptInBrightScript": true,
|
||||
"stagingFolderPath": "build",
|
||||
"plugins": ["rooibos-roku"],
|
||||
"rooibos": {
|
||||
"isRecordingCodeCoverage": false,
|
||||
"testsFilePattern": null,
|
||||
"showOnlyFailures": true,
|
||||
"catchCrashes": true,
|
||||
"lineWidth": 70,
|
||||
"failFast": false,
|
||||
"sendHomeOnFinish": false
|
||||
},
|
||||
"sourceMap": true
|
||||
}
|
|
@ -1,19 +1,22 @@
|
|||
{
|
||||
"files": [
|
||||
"manifest",
|
||||
"source/**/*.*",
|
||||
"components/**/*.*",
|
||||
"images/**/*.*",
|
||||
"resources/**/*.*",
|
||||
"locale/**/*.*",
|
||||
"settings/*.*"
|
||||
],
|
||||
"plugins": [
|
||||
"@rokucommunity/bslint"
|
||||
],
|
||||
"diagnosticFilters": [
|
||||
"**/roku_modules/**/*",
|
||||
"**/testFramework/*",
|
||||
"**/tests/*"
|
||||
]
|
||||
}
|
||||
"files": [
|
||||
"manifest",
|
||||
"source/**/*.*",
|
||||
"components/**/*.*",
|
||||
"images/**/*.*",
|
||||
"resources/**/*.*",
|
||||
"locale/**/*.*",
|
||||
"settings/*.*"
|
||||
],
|
||||
"plugins": ["@rokucommunity/bslint", "roku-log-bsc-plugin"],
|
||||
"rokuLog": {
|
||||
"strip": false,
|
||||
"insertPkgPath": true,
|
||||
"removeComments": false
|
||||
},
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"sourceMap": true,
|
||||
"autoImportComponentScript": true,
|
||||
"stagingDir": "build/staging",
|
||||
"retainStagingDir": true
|
||||
}
|
||||
|
|
14
bsfmt.json
14
bsfmt.json
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"source/**/*.brs",
|
||||
"components/**/*.brs"
|
||||
]
|
||||
}
|
||||
"files": [
|
||||
"source/**/*.brs",
|
||||
"source/**/*.bs",
|
||||
"components/**/*.brs",
|
||||
"components/**/*.bs",
|
||||
"test-app/**/*.bs",
|
||||
"!**/roku_modules/**/*.*"
|
||||
]
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="ButtonGroupHoriz" extends="ButtonGroup">
|
||||
<interface>
|
||||
<field id="escape" type="string" alwaysNotify="true" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="ButtonGroupHoriz.brs" />
|
||||
|
||||
|
||||
</component>
|
||||
</component>
|
44
components/ButtonGroupVert.bs
Normal file
44
components/ButtonGroupVert.bs
Normal file
|
@ -0,0 +1,44 @@
|
|||
sub init()
|
||||
m.top.layoutDirection = "vert"
|
||||
m.top.observeField("focusedChild", "onFocusChanged")
|
||||
m.top.observeField("focusButton", "onFocusButtonChanged")
|
||||
end sub
|
||||
|
||||
sub onFocusChanged()
|
||||
if m.top.hasFocus()
|
||||
m.top.getChild(0).setFocus(true)
|
||||
m.top.focusButton = 0
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onFocusButtonChanged()
|
||||
m.top.getChild(m.top.focusButton).setFocus(true)
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if key = "OK"
|
||||
m.top.selected = m.top.focusButton
|
||||
return true
|
||||
end if
|
||||
|
||||
if not press then return false
|
||||
|
||||
if key = "down"
|
||||
i = m.top.focusButton
|
||||
target = i + 1
|
||||
if target >= m.top.getChildCount() then return false
|
||||
m.top.focusButton = target
|
||||
return true
|
||||
else if key = "up"
|
||||
i = m.top.focusButton
|
||||
target = i - 1
|
||||
if target < 0 then return false
|
||||
m.top.focusButton = target
|
||||
return true
|
||||
else if key = "left" or key = "right"
|
||||
m.top.escape = key
|
||||
return true
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
7
components/ButtonGroupVert.xml
Normal file
7
components/ButtonGroupVert.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="ButtonGroupVert" extends="ButtonGroup">
|
||||
<interface>
|
||||
<field id="escape" type="string" alwaysNotify="true" />
|
||||
<field id="selected" type="integer" alwaysNotify="true" />
|
||||
</interface>
|
||||
</component>
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFButtons" extends="Group">
|
||||
<children>
|
||||
<Group>
|
||||
|
@ -8,13 +8,13 @@
|
|||
|
||||
<rectangle id="focus" />
|
||||
|
||||
<LayoutGroup id="buttonGroup" layoutDirection="horiz" itemSpacings = "[75]" translation="[50,20]">
|
||||
<LayoutGroup id="buttonGroup" layoutDirection="horiz" itemSpacings="[75]" translation="[50,20]">
|
||||
</LayoutGroup>
|
||||
|
||||
<Animation id="moveFocusAnimation" duration="0.25" repeat="false" easeFunction="outQuad">
|
||||
<FloatFieldInterpolator id = "focusWidth" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="focus.width" />
|
||||
<FloatFieldInterpolator id = "focusHeight" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="focus.height" />
|
||||
<Vector2DFieldInterpolator id = "focusLocation" key="[0.0, 1.0]" keyValue="[]" fieldToInterp="focus.translation" />
|
||||
<FloatFieldInterpolator id="focusWidth" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="focus.width" />
|
||||
<FloatFieldInterpolator id="focusHeight" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="focus.height" />
|
||||
<Vector2DFieldInterpolator id="focusLocation" key="[0.0, 1.0]" keyValue="[]" fieldToInterp="focus.translation" />
|
||||
</Animation>
|
||||
</Group>
|
||||
|
||||
|
@ -25,5 +25,4 @@
|
|||
<field id="focusedIndex" type="integer" alwaysNotify="true" />
|
||||
<field id="selectedIndex" type="integer" onChange="selectedIndexChanged" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFButtons.brs" />
|
||||
</component>
|
||||
</component>
|
100
components/Buttons/SlideOutButton.bs
Normal file
100
components/Buttons/SlideOutButton.bs
Normal file
|
@ -0,0 +1,100 @@
|
|||
sub init()
|
||||
m.buttonBackground = m.top.findNode("buttonBackground")
|
||||
m.buttonIcon = m.top.findNode("buttonIcon")
|
||||
m.buttonText = m.top.findNode("buttonText")
|
||||
|
||||
m.buttonText.visible = false
|
||||
|
||||
m.originalWidth = 0
|
||||
|
||||
m.top.observeField("background", "onBackgroundChanged")
|
||||
m.top.observeField("icon", "onIconChanged")
|
||||
m.top.observeField("text", "onTextChanged")
|
||||
m.top.observeField("height", "onHeightChanged")
|
||||
m.top.observeField("width", "onWidthChanged")
|
||||
m.top.observeField("padding", "onPaddingChanged")
|
||||
m.top.observeField("focusedChild", "onFocusChanged")
|
||||
|
||||
m.top.observeField("highlighted", "onHighlightChanged")
|
||||
end sub
|
||||
|
||||
sub onFocusChanged()
|
||||
if m.top.hasFocus()
|
||||
m.buttonText.visible = true
|
||||
m.buttonBackground.blendColor = m.top.focusBackground
|
||||
m.top.width = 250
|
||||
else
|
||||
m.buttonText.visible = false
|
||||
m.top.width = m.originalWidth
|
||||
onHighlightChanged()
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onHighlightChanged()
|
||||
if m.top.highlighted
|
||||
m.buttonBackground.blendColor = m.top.highlightBackground
|
||||
else
|
||||
m.buttonBackground.blendColor = m.top.background
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onBackgroundChanged()
|
||||
m.buttonBackground.blendColor = m.top.background
|
||||
m.top.unobserveField("background")
|
||||
end sub
|
||||
|
||||
sub onIconChanged()
|
||||
m.buttonIcon.uri = m.top.icon
|
||||
end sub
|
||||
|
||||
sub onTextChanged()
|
||||
m.buttonText.text = m.top.text
|
||||
end sub
|
||||
|
||||
sub setIconSize()
|
||||
height = m.buttonBackground.height
|
||||
width = m.buttonBackground.width
|
||||
if height > 0 and width > 0
|
||||
' TODO: Use smallest number between them
|
||||
m.buttonIcon.height = m.top.height
|
||||
|
||||
if m.top.padding > 0
|
||||
m.buttonIcon.height = m.buttonIcon.height - m.top.padding
|
||||
end if
|
||||
|
||||
m.buttonIcon.width = m.buttonIcon.height
|
||||
|
||||
m.buttonIcon.translation = [m.top.padding, ((height - m.buttonIcon.height) / 2)]
|
||||
m.buttonText.translation = [m.top.padding + m.buttonIcon.width + 10, 12]
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onHeightChanged()
|
||||
m.buttonBackground.height = m.top.height
|
||||
setIconSize()
|
||||
end sub
|
||||
|
||||
sub onWidthChanged()
|
||||
if m.originalWidth = 0
|
||||
m.originalWidth = m.top.width
|
||||
end if
|
||||
|
||||
m.buttonBackground.width = m.top.width
|
||||
setIconSize()
|
||||
end sub
|
||||
|
||||
sub onPaddingChanged()
|
||||
setIconSize()
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if not press then return false
|
||||
|
||||
if key = "OK" and m.top.hasFocus()
|
||||
' Simply toggle the selected field to trigger the next event
|
||||
m.top.selected = not m.top.selected
|
||||
return true
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
20
components/Buttons/SlideOutButton.xml
Normal file
20
components/Buttons/SlideOutButton.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="SlideOutButton" extends="Group">
|
||||
<children>
|
||||
<Poster id="buttonBackground" uri="pkg:/images/white.9.png" />
|
||||
<Poster id="buttonIcon" />
|
||||
<Label id="buttonText" color="#ffffff" font="font:SmallestSystemFont" horizAlign="center" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="background" type="color" value="" />
|
||||
<field id="focusBackground" type="color" value="" />
|
||||
<field id="highlightBackground" type="color" value="" />
|
||||
<field id="text" type="string" value="" />
|
||||
<field id="padding" type="integer" value="-1" />
|
||||
<field id="height" type="integer" value="" />
|
||||
<field id="width" type="integer" value="" />
|
||||
<field id="icon" type="string" value="" />
|
||||
<field id="selected" type="boolean" value="false" />
|
||||
<field id="highlighted" type="boolean" value="false" />
|
||||
</interface>
|
||||
</component>
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="TextSizeTask" extends="Task">
|
||||
<interface>
|
||||
<field id="fontName" type="string" />
|
||||
<field id="fontSize" type="int" />
|
||||
<field id="text" type="array" />
|
||||
<field id="text" type="array" />
|
||||
<field id="maxWidth" type="int" value="1920" />
|
||||
<field id="name" type="string" />
|
||||
|
||||
|
@ -12,5 +12,4 @@
|
|||
<field id="width" type="array" />
|
||||
<field id="height" type="int" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="TextSizeTask.brs" />
|
||||
</component>
|
39
components/Clock.bs
Normal file
39
components/Clock.bs
Normal file
|
@ -0,0 +1,39 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
|
||||
' If hideclick setting is checked, exit without setting any variables
|
||||
if m.global.session.user.settings["ui.design.hideclock"]
|
||||
return
|
||||
end if
|
||||
|
||||
m.clockTime = m.top.findNode("clockTime")
|
||||
|
||||
m.currentTimeTimer = m.top.findNode("currentTimeTimer")
|
||||
m.dateTimeObject = CreateObject("roDateTime")
|
||||
|
||||
m.currentTimeTimer.observeField("fire", "onCurrentTimeTimerFire")
|
||||
m.currentTimeTimer.control = "start"
|
||||
|
||||
' Default to 12 hour clock
|
||||
m.format = "short-h12"
|
||||
|
||||
' If user has selected a 24 hour clock, update date display format
|
||||
if LCase(m.global.device.clockFormat) = "24h"
|
||||
m.format = "short-h24"
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
' onCurrentTimeTimerFire: Code that runs every time the currentTimeTimer fires
|
||||
'
|
||||
sub onCurrentTimeTimerFire()
|
||||
' Refresh time variable
|
||||
m.dateTimeObject.Mark()
|
||||
|
||||
' Convert to local time zone
|
||||
m.dateTimeObject.ToLocalTime()
|
||||
|
||||
' Format time as requested
|
||||
m.clockTime.text = m.dateTimeObject.asTimeStringLoc(m.format)
|
||||
end sub
|
9
components/Clock.xml
Normal file
9
components/Clock.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="Clock" extends="Group">
|
||||
<children>
|
||||
<Label id="clockTime" font="font:SmallSystemFont" horizAlign="right" vertAlign="center" height="64" width="200" />
|
||||
<Timer id="currentTimeTimer" repeat="true" duration="1" />
|
||||
</children>
|
||||
<interface>
|
||||
</interface>
|
||||
</component>
|
|
@ -1,10 +1,13 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/sdk.bs"
|
||||
|
||||
sub init()
|
||||
m.top.functionName = "getNextEpisodeTask"
|
||||
end sub
|
||||
|
||||
sub getNextEpisodeTask()
|
||||
m.nextEpisodeData = api_API().shows.getepisodes(m.top.showID, {
|
||||
UserId: get_setting("active_user"),
|
||||
m.nextEpisodeData = api.shows.GetEpisodes(m.top.showID, {
|
||||
UserId: m.global.session.user.id,
|
||||
StartItemId: m.top.videoID,
|
||||
Limit: 2
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="GetNextEpisodeTask" extends="Task">
|
||||
<interface>
|
||||
|
@ -6,7 +6,4 @@
|
|||
<field id="showID" type="string" />
|
||||
<field id="nextEpisodeData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetNextEpisodeTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
|
@ -1,26 +1,32 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/api/sdk.bs"
|
||||
|
||||
sub init()
|
||||
m.top.functionName = "getPlaybackInfoTask"
|
||||
end sub
|
||||
|
||||
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
|
||||
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
|
||||
currentView = m.global.sceneManager.callFunc("getActiveScene")
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
|
||||
body = {
|
||||
"DeviceProfile": getDeviceProfile()
|
||||
}
|
||||
params = {
|
||||
"UserId": get_setting("active_user"),
|
||||
"StartTimeTicks": startTimeTicks,
|
||||
"UserId": m.global.session.user.id,
|
||||
"StartTimeTicks": currentItem.startingPoint,
|
||||
"IsPlayback": true,
|
||||
"AutoOpenLiveStream": true,
|
||||
"MaxStreamingBitrate": "140000000",
|
||||
"MaxStaticBitrate": "140000000",
|
||||
"SubtitleStreamIndex": subtitleTrackIndex
|
||||
"SubtitleStreamIndex": currentView.selectedSubtitle,
|
||||
"MediaSourceId": currentItem.id,
|
||||
"AudioStreamIndex": currentItem.selectedAudioStreamIndex
|
||||
}
|
||||
|
||||
mediaSourceId = id
|
||||
if mediaSourceId <> "" then params.MediaSourceId = mediaSourceId
|
||||
|
||||
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
|
||||
|
||||
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
||||
req.SetRequest("POST")
|
||||
return postJson(req, FormatJson(body))
|
||||
|
@ -29,7 +35,7 @@ end function
|
|||
' Returns an array of playback info to be displayed during playback.
|
||||
' In the future, with a custom playback info view, we can return an associated array.
|
||||
sub getPlaybackInfoTask()
|
||||
sessions = api_API().sessions.get()
|
||||
sessions = api.sessions.Get({ "deviceId": m.global.device.serverDeviceName })
|
||||
|
||||
m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
|
||||
|
||||
|
@ -40,15 +46,15 @@ sub getPlaybackInfoTask()
|
|||
end if
|
||||
end sub
|
||||
|
||||
function GetTranscodingStats(session)
|
||||
function GetTranscodingStats(deviceSession)
|
||||
sessionStats = { data: [] }
|
||||
|
||||
if isValid(session.TranscodingInfo) and session.TranscodingInfo.Count() > 0
|
||||
transcodingReasons = session.TranscodingInfo.TranscodeReasons
|
||||
videoCodec = session.TranscodingInfo.VideoCodec
|
||||
audioCodec = session.TranscodingInfo.AudioCodec
|
||||
totalBitrate = session.TranscodingInfo.Bitrate
|
||||
audioChannels = session.TranscodingInfo.AudioChannels
|
||||
if isValid(deviceSession.TranscodingInfo) and deviceSession.TranscodingInfo.Count() > 0
|
||||
transcodingReasons = deviceSession.TranscodingInfo.TranscodeReasons
|
||||
videoCodec = deviceSession.TranscodingInfo.VideoCodec
|
||||
audioCodec = deviceSession.TranscodingInfo.AudioCodec
|
||||
totalBitrate = deviceSession.TranscodingInfo.Bitrate
|
||||
audioChannels = deviceSession.TranscodingInfo.AudioChannels
|
||||
|
||||
if isValid(transcodingReasons) and transcodingReasons.Count() > 0
|
||||
sessionStats.data.push("<header>" + tr("Transcoding Information") + "</header>")
|
||||
|
@ -59,7 +65,7 @@ function GetTranscodingStats(session)
|
|||
|
||||
if isValid(videoCodec)
|
||||
data = "<b>• " + tr("Video Codec") + ":</b> " + videoCodec
|
||||
if session.TranscodingInfo.IsVideoDirect
|
||||
if deviceSession.TranscodingInfo.IsVideoDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.data.push(data)
|
||||
|
@ -67,7 +73,7 @@ function GetTranscodingStats(session)
|
|||
|
||||
if isValid(audioCodec)
|
||||
data = "<b>• " + tr("Audio Codec") + ":</b> " + audioCodec
|
||||
if session.TranscodingInfo.IsAudioDirect
|
||||
if deviceSession.TranscodingInfo.IsAudioDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.data.push(data)
|
||||
|
@ -82,6 +88,9 @@ function GetTranscodingStats(session)
|
|||
data = "<b>• " + tr("Audio Channels") + ":</b> " + Str(audioChannels)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
else
|
||||
sessionStats.data.push("<header>" + tr("Direct playing") + "</header>")
|
||||
sessionStats.data.push("<b>" + tr("The source file is entirely compatible with this client and the session is receiving the file without modifications.") + "</b>")
|
||||
end if
|
||||
|
||||
if havePlaybackInfo()
|
|
@ -1,14 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="GetPlaybackInfoTask" extends="Task">
|
||||
<interface>
|
||||
<field id="videoID" type="string" />
|
||||
<field id="data" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetPlaybackInfoTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
|
@ -1,10 +1,13 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/sdk.bs"
|
||||
|
||||
sub init()
|
||||
m.top.functionName = "getShuffleEpisodesTask"
|
||||
end sub
|
||||
|
||||
sub getShuffleEpisodesTask()
|
||||
data = api_API().shows.getepisodes(m.top.showID, {
|
||||
UserId: get_setting("active_user"),
|
||||
data = api.shows.GetEpisodes(m.top.showID, {
|
||||
UserId: m.global.session.user.id,
|
||||
SortBy: "Random",
|
||||
Limit: 200
|
||||
})
|
|
@ -1,11 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="GetShuffleEpisodesTask" extends="Task">
|
||||
<interface>
|
||||
<field id="showID" type="string" />
|
||||
<field id="data" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetShuffleEpisodesTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="IconButton" extends="Group">
|
||||
<children>
|
||||
<Poster id="buttonBackground" uri="pkg:/images/white.9.png" />
|
||||
|
@ -17,5 +17,4 @@
|
|||
<field id="focus" type="boolean" />
|
||||
<field id="escape" type="string" value="" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="IconButton.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -24,5 +24,20 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
end if
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "up"
|
||||
if m.Alphamenu.itemFocused = 0
|
||||
m.Alphamenu.jumpToItem = m.Alphamenu.numRows - 1
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
if key = "down"
|
||||
if m.Alphamenu.itemFocused = m.Alphamenu.numRows - 1
|
||||
m.Alphamenu.jumpToItem = 0
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
|
@ -1,55 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="Alpha" extends = "Group">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="Alpha" extends="Group">
|
||||
<children>
|
||||
<LabelList
|
||||
translation="[50, 185]"
|
||||
vertFocusAnimationStyle="floatingFocus"
|
||||
drawFocusFeedback="true"
|
||||
id = "Alphamenu"
|
||||
font="font:SmallestSystemFont"
|
||||
itemSpacing="[0,2]"
|
||||
itemSize="[28,28]"
|
||||
numRows="27"
|
||||
textHorizAlign="center"
|
||||
focusBitmapUri = "pkg:/images/white.png"
|
||||
focusBitmapBlendColor = "#FFFFFF"
|
||||
focusFootprintBlendColor = "#666666"
|
||||
<LabelList
|
||||
translation="[50, 185]"
|
||||
vertFocusAnimationStyle="floatingFocus"
|
||||
drawFocusFeedback="true"
|
||||
id="Alphamenu"
|
||||
font="font:SmallestSystemFont"
|
||||
itemSpacing="[0,2]"
|
||||
itemSize="[28,28]"
|
||||
numRows="27"
|
||||
textHorizAlign="center"
|
||||
focusBitmapUri="pkg:/images/white.png"
|
||||
focusBitmapBlendColor="#FFFFFF"
|
||||
focusFootprintBlendColor="#666666"
|
||||
>
|
||||
<ContentNode id="alphatext" role = "content" >
|
||||
<ContentNode title = "#" />
|
||||
<ContentNode title = "A" />
|
||||
<ContentNode title = "B" />
|
||||
<ContentNode title = "C" />
|
||||
<ContentNode title = "D" />
|
||||
<ContentNode title = "E" />
|
||||
<ContentNode title = "F" />
|
||||
<ContentNode title = "G" />
|
||||
<ContentNode title = "H" />
|
||||
<ContentNode title = "I" />
|
||||
<ContentNode title = "J" />
|
||||
<ContentNode title = "K" />
|
||||
<ContentNode title = "L" />
|
||||
<ContentNode title = "M" />
|
||||
<ContentNode title = "N" />
|
||||
<ContentNode title = "O" />
|
||||
<ContentNode title = "P" />
|
||||
<ContentNode title = "Q" />
|
||||
<ContentNode title = "R" />
|
||||
<ContentNode title = "S" />
|
||||
<ContentNode title = "T" />
|
||||
<ContentNode title = "U" />
|
||||
<ContentNode title = "V" />
|
||||
<ContentNode title = "W" />
|
||||
<ContentNode title = "X" />
|
||||
<ContentNode title = "Y" />
|
||||
<ContentNode title = "Z" />
|
||||
</ContentNode>
|
||||
</LabelList>
|
||||
<ContentNode id="alphatext" role="content">
|
||||
<ContentNode title="#" />
|
||||
<ContentNode title="A" />
|
||||
<ContentNode title="B" />
|
||||
<ContentNode title="C" />
|
||||
<ContentNode title="D" />
|
||||
<ContentNode title="E" />
|
||||
<ContentNode title="F" />
|
||||
<ContentNode title="G" />
|
||||
<ContentNode title="H" />
|
||||
<ContentNode title="I" />
|
||||
<ContentNode title="J" />
|
||||
<ContentNode title="K" />
|
||||
<ContentNode title="L" />
|
||||
<ContentNode title="M" />
|
||||
<ContentNode title="N" />
|
||||
<ContentNode title="O" />
|
||||
<ContentNode title="P" />
|
||||
<ContentNode title="Q" />
|
||||
<ContentNode title="R" />
|
||||
<ContentNode title="S" />
|
||||
<ContentNode title="T" />
|
||||
<ContentNode title="U" />
|
||||
<ContentNode title="V" />
|
||||
<ContentNode title="W" />
|
||||
<ContentNode title="X" />
|
||||
<ContentNode title="Y" />
|
||||
<ContentNode title="Z" />
|
||||
</ContentNode>
|
||||
</LabelList>
|
||||
</children>
|
||||
<interface>
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
<field id="focusedChild" type="node" onChange="focusChanged" />
|
||||
<field id="itemAlphaSelected" type="string" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="Alpha.brs" />
|
||||
</component>
|
|
@ -1,3 +1,7 @@
|
|||
import "pkg:/source/api/UserLibrary.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.top.functionName = "setFavoriteStatus"
|
||||
end sub
|
|
@ -1,12 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="FavoriteItemsTask" extends="Task">
|
||||
<interface>
|
||||
<field id="itemId" type="string" />
|
||||
<field id="favTask" type="string" value="" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="FavoriteItemsTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/UserLibrary.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
|
@ -1,4 +1,9 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||
|
||||
sub init()
|
||||
m.log = log.Logger("GridItem")
|
||||
m.posterMask = m.top.findNode("posterMask")
|
||||
m.itemPoster = m.top.findNode("itemPoster")
|
||||
m.itemIcon = m.top.findNode("itemIcon")
|
||||
|
@ -13,7 +18,7 @@ sub init()
|
|||
|
||||
m.itemText.translation = [0, m.itemPoster.height + 7]
|
||||
|
||||
m.gridTitles = get_user_setting("itemgrid.gridTitles")
|
||||
m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
||||
m.itemText.visible = m.gridTitles = "showalways"
|
||||
|
||||
' Add some padding space when Item Titles are always showing
|
||||
|
@ -43,11 +48,14 @@ sub itemContentChanged()
|
|||
m.itemIcon.uri = itemData.iconUrl
|
||||
m.itemText.text = itemData.Title
|
||||
else if itemData.type = "Series"
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||
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
|
||||
else
|
||||
m.unplayedCount.visible = false
|
||||
m.unplayedEpisodeCount.text = ""
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
@ -117,7 +125,7 @@ sub itemContentChanged()
|
|||
m.posterText.height = 200
|
||||
m.posterText.width = 280
|
||||
else
|
||||
print "Unhandled Grid Item Type: " + itemData.type
|
||||
m.log.warn("Unhandled Grid Item Type", itemData.type)
|
||||
end if
|
||||
|
||||
'If Poster not loaded, ensure "blue box" is shown until loaded
|
|
@ -4,8 +4,8 @@
|
|||
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]">
|
||||
<Poster id="backdrop" width="290" height="425" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
|
||||
<Poster id="itemPoster" width="290" height="425" loadDisplayMode="scaleToZoom">
|
||||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[201, 0]">
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" opacity=".99" translation="[201, 0]">
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
</Poster>
|
||||
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
|
||||
|
@ -18,7 +18,4 @@
|
|||
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
|
||||
<field id="focusPercent" type="float" onChange="focusChanging" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GridItem.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
|
@ -1,3 +1,6 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.itemPoster = m.top.findNode("itemPoster")
|
||||
m.posterText = m.top.findNode("posterText")
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="GridItemSmall" extends="Group">
|
||||
<children>
|
||||
<Poster id="backdrop" translation="[0,15]" width="230" height="320" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
|
||||
|
@ -11,7 +11,4 @@
|
|||
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
||||
<field id="itemHasFocus" type="boolean" onChange="focusChanged" alwaysNotify="true" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GridItemSmall.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,8 +1,14 @@
|
|||
sub init()
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||
|
||||
sub init()
|
||||
m.log = log.Logger("ItemGrid")
|
||||
m.options = m.top.findNode("options")
|
||||
|
||||
m.showItemCount = get_user_setting("itemgrid.showItemCount") = "true"
|
||||
m.showItemCount = m.global.session.user.settings["itemgrid.showItemCount"]
|
||||
|
||||
m.tvGuide = invalid
|
||||
m.channelFocused = invalid
|
||||
|
@ -58,22 +64,16 @@ sub init()
|
|||
'set inital counts for overhang before content is loaded.
|
||||
m.loadItemsTask.totalRecordCount = 0
|
||||
|
||||
m.spinner = m.top.findNode("spinner")
|
||||
m.spinner.visible = true
|
||||
|
||||
m.Alpha = m.top.findNode("AlphaMenu")
|
||||
m.AlphaSelected = m.top.findNode("AlphaSelected")
|
||||
|
||||
'Get reset folder setting
|
||||
m.resetGrid = get_user_setting("itemgrid.reset") = "true"
|
||||
m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
|
||||
|
||||
'Check if device has voice remote
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
m.deviFeature = devinfo.HasFeature("voice_remote")
|
||||
m.micButton = m.top.findNode("micButton")
|
||||
m.micButtonText = m.top.findNode("micButtonText")
|
||||
'Hide voice search if device does not have voice remote
|
||||
if m.deviFeature = false
|
||||
if m.global.device.hasVoiceRemote = false
|
||||
m.micButton.visible = false
|
||||
m.micButtonText.visible = false
|
||||
end if
|
||||
|
@ -89,7 +89,7 @@ end sub
|
|||
'Load initial set of Data
|
||||
sub loadInitialItems()
|
||||
m.loadItemsTask.control = "stop"
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner()
|
||||
|
||||
if m.top.parentItem.json.Type = "CollectionFolder" 'or m.top.parentItem.json.Type = "Folder"
|
||||
m.top.HomeLibraryItem = m.top.parentItem.Id
|
||||
|
@ -102,9 +102,9 @@ sub loadInitialItems()
|
|||
' Read view/sort/filter settings
|
||||
if m.top.parentItem.collectionType = "livetv"
|
||||
' Translate between app and server nomenclature
|
||||
viewSetting = get_user_setting("display.livetv.landing")
|
||||
viewSetting = m.global.session.user.settings["display.livetv.landing"]
|
||||
'Move mic to be visiable on TV Guide screen
|
||||
if m.deviFeature = true
|
||||
if m.global.device.hasVoiceRemote = true
|
||||
m.micButton.translation = "[1540, 92]"
|
||||
m.micButtonText.visible = true
|
||||
m.micButtonText.translation = "[1600,130]"
|
||||
|
@ -116,25 +116,33 @@ sub loadInitialItems()
|
|||
else
|
||||
m.view = "livetv"
|
||||
end if
|
||||
m.sortField = get_user_setting("display.livetv.sortField")
|
||||
sortAscendingStr = get_user_setting("display.livetv.sortAscending")
|
||||
m.filter = get_user_setting("display.livetv.filter")
|
||||
m.sortField = m.global.session.user.settings["display.livetv.sortField"]
|
||||
sortAscendingStr = m.global.session.user.settings["display.livetv.sortAscending"]
|
||||
m.filter = m.global.session.user.settings["display.livetv.filter"]
|
||||
else if m.top.parentItem.collectionType = "music"
|
||||
m.view = get_user_setting("display.music.view")
|
||||
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
|
||||
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
|
||||
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
|
||||
m.view = m.global.session.user.settings["display.music.view"]
|
||||
m.sortField = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortField"]
|
||||
sortAscendingStr = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortAscending"]
|
||||
m.filter = m.global.session.user.settings["display." + m.top.parentItem.Id + ".filter"]
|
||||
else
|
||||
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
|
||||
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
|
||||
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
m.sortField = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortField"]
|
||||
sortAscendingStr = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortAscending"]
|
||||
m.filter = m.global.session.user.settings["display." + m.top.parentItem.Id + ".filter"]
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
end if
|
||||
|
||||
if m.sortField = invalid
|
||||
' Set the default order for boxsets to the Release Date - API calls it PremiereDate
|
||||
if LCase(m.top.parentItem.json.Type) = "boxset"
|
||||
m.sortField = "PremiereDate"
|
||||
else
|
||||
m.sortField = "SortName"
|
||||
end if
|
||||
end if
|
||||
|
||||
if m.sortField = invalid then m.sortField = "SortName"
|
||||
if m.filter = invalid then m.filter = "All"
|
||||
|
||||
if sortAscendingStr = invalid or sortAscendingStr = "true"
|
||||
if sortAscendingStr = invalid or sortAscendingStr = true
|
||||
m.sortAscending = true
|
||||
else
|
||||
m.sortAscending = false
|
||||
|
@ -180,7 +188,7 @@ sub loadInitialItems()
|
|||
m.loadItemsTask.itemType = "MusicArtist"
|
||||
m.loadItemsTask.itemId = m.top.parentItem.Id
|
||||
|
||||
m.view = get_user_setting("display.music.view")
|
||||
m.view = m.global.session.user.settings["display.music.view"]
|
||||
|
||||
if m.view = "music-album"
|
||||
m.loadItemsTask.itemType = "MusicAlbum"
|
||||
|
@ -191,7 +199,7 @@ sub loadInitialItems()
|
|||
' For LiveTV, we want to "Fit" the item images, not zoom
|
||||
m.top.imageDisplayMode = "scaleToFit"
|
||||
|
||||
if get_user_setting("display.livetv.landing") = "guide" and m.options.view <> "livetv"
|
||||
if m.global.session.user.settings["display.livetv.landing"] = "guide" and m.options.view <> "livetv"
|
||||
showTvGuide()
|
||||
end if
|
||||
else if m.top.parentItem.collectionType = "CollectionFolder" or m.top.parentItem.type = "CollectionFolder" or m.top.parentItem.collectionType = "boxsets" or m.top.parentItem.Type = "Boxset" or m.top.parentItem.Type = "Boxsets" or m.top.parentItem.Type = "Folder" or m.top.parentItem.Type = "Channel"
|
||||
|
@ -209,7 +217,7 @@ sub loadInitialItems()
|
|||
m.loadItemsTask.itemType = "Series,Movie"
|
||||
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
|
||||
else
|
||||
print "[ItemGrid] Unknown Type: " m.top.parentItem
|
||||
m.log.warn("Unknown Item Type", m.top.parentItem)
|
||||
end if
|
||||
|
||||
if m.top.parentItem.type <> "Folder" and (m.options.view = "Networks" or m.view = "Networks" or m.options.view = "Studios" or m.view = "Studios")
|
||||
|
@ -227,7 +235,7 @@ sub loadInitialItems()
|
|||
end if
|
||||
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner(false)
|
||||
m.loadItemsTask.control = "RUN"
|
||||
SetUpOptions()
|
||||
end sub
|
||||
|
@ -439,6 +447,7 @@ end sub
|
|||
'
|
||||
'Handle loaded data, and add to Grid
|
||||
sub ItemDataLoaded(msg)
|
||||
stopLoadingSpinner()
|
||||
m.top.alphaActive = false
|
||||
itemData = msg.GetData()
|
||||
m.loadItemsTask.unobserveField("content")
|
||||
|
@ -464,7 +473,7 @@ sub ItemDataLoaded(msg)
|
|||
m.genreList.setFocus(true)
|
||||
|
||||
m.loading = false
|
||||
m.spinner.visible = false
|
||||
stopLoadingSpinner()
|
||||
return
|
||||
end if
|
||||
|
||||
|
@ -487,7 +496,7 @@ sub ItemDataLoaded(msg)
|
|||
|
||||
m.itemGrid.setFocus(true)
|
||||
m.genreList.setFocus(false)
|
||||
m.spinner.visible = false
|
||||
stopLoadingSpinner()
|
||||
end sub
|
||||
|
||||
'
|
||||
|
@ -560,8 +569,9 @@ end sub
|
|||
'
|
||||
'Load next set of items
|
||||
sub loadMoreData()
|
||||
m.spinner.visible = true
|
||||
if m.Loading = true then return
|
||||
|
||||
startLoadingSpinner(false)
|
||||
m.Loading = true
|
||||
m.loadItemsTask.startIndex = m.loadedItems
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
|
@ -583,7 +593,7 @@ sub onItemalphaSelected()
|
|||
m.loadItemsTask.searchTerm = ""
|
||||
m.VoiceBox.text = ""
|
||||
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner(false)
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -598,7 +608,7 @@ sub onvoiceFilter()
|
|||
m.loadItemsTask.NameStartsWith = " "
|
||||
m.loadItemsTask.searchTerm = m.voiceBox.text
|
||||
m.loadItemsTask.recursive = true
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner(false)
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -647,7 +657,7 @@ sub optionsClosed()
|
|||
reload = true
|
||||
end if
|
||||
else
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
if m.options.view <> m.view
|
||||
'reload and store new view setting
|
||||
m.view = m.options.view
|
||||
|
@ -714,6 +724,7 @@ sub showTVGuide()
|
|||
m.tvGuide.filter = m.filter
|
||||
m.tvGuide.searchTerm = m.voiceBox.text
|
||||
m.top.appendChild(m.tvGuide)
|
||||
m.scheduleGrid = m.top.findNode("scheduleGrid")
|
||||
m.tvGuide.lastFocus.setFocus(true)
|
||||
end sub
|
||||
|
||||
|
@ -723,6 +734,9 @@ sub onChannelSelected(msg)
|
|||
if node.watchChannel <> invalid
|
||||
' Clone the node when it's reused/update in the TimeGrid it doesn't automatically start playing
|
||||
m.top.selectedItem = node.watchChannel.clone(false)
|
||||
' Make sure to set watchChanel to invalid in case the user hits back and then selects
|
||||
' the same channel on the guide (without moving away from the currently selected channel)
|
||||
m.tvGuide.watchChannel = invalid
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -731,6 +745,18 @@ sub onChannelFocused(msg)
|
|||
m.channelFocused = node.focusedChannel
|
||||
end sub
|
||||
|
||||
'Returns Focused Item
|
||||
function getItemFocused()
|
||||
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||
else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused)
|
||||
return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1])
|
||||
else if m.scheduleGrid.isinFocusChain() and isValid(m.scheduleGrid.itemFocused)
|
||||
return m.scheduleGrid.content.getChild(m.scheduleGrid.itemFocused)
|
||||
end if
|
||||
return invalid
|
||||
end function
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if not press then return false
|
||||
|
||||
|
@ -777,21 +803,25 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.loadItemsTask.control = "stop"
|
||||
return true
|
||||
end if
|
||||
else if key = "play" or key = "OK"
|
||||
else if key = "OK"
|
||||
markupGrid = m.top.findNode("itemGrid")
|
||||
itemToPlay = markupGrid.content.getChild(markupGrid.itemFocused)
|
||||
itemToPlay = getItemFocused()
|
||||
|
||||
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
return true
|
||||
else if itemToPlay <> invalid and itemToPlay.type = "Photo"
|
||||
if itemToPlay <> invalid and itemToPlay.type = "Photo"
|
||||
' Spawn photo player task
|
||||
photoPlayer = CreateObject("roSgNode", "PhotoDetails")
|
||||
photoPlayer.items = markupGrid
|
||||
photoPlayer.itemsNode = markupGrid
|
||||
photoPlayer.itemIndex = markupGrid.itemFocused
|
||||
m.global.sceneManager.callfunc("pushScene", photoPlayer)
|
||||
return true
|
||||
end if
|
||||
else if key = "play"
|
||||
itemToPlay = getItemFocused()
|
||||
|
||||
if itemToPlay <> invalid
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
return true
|
||||
end if
|
||||
else if key = "left" and topGrp.isinFocusChain()
|
||||
m.top.alphaActive = true
|
||||
topGrp.setFocus(false)
|
||||
|
@ -814,7 +844,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
end if
|
||||
|
||||
if key = "replay"
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.searchTerm = ""
|
||||
m.loadItemsTask.nameStartsWith = ""
|
||||
m.voiceBox.text = ""
|
|
@ -1,36 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="ItemGrid" extends="JFGroup">
|
||||
<children>
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x262626ff" translation = "[25, 75]" />
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width="40" translation="[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x262626ff" translation="[25, 75]" />
|
||||
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1920" height="1080" opacity="0.25" />
|
||||
<poster id="backdropTransition" loadDisplayMode="scaleToFill" width="1920" height="1080" opacity="0.25" />
|
||||
<MarkupGrid
|
||||
id = "itemGrid"
|
||||
translation = "[ 96, 160 ]"
|
||||
itemComponentName = "GridItem"
|
||||
numColumns = "6"
|
||||
numRows = "5"
|
||||
vertFocusAnimationStyle = "fixed"
|
||||
itemSize = "[ 290, 425 ]"
|
||||
itemSpacing = "[ 0, 45 ]"
|
||||
drawFocusFeedback = "false" />
|
||||
<MarkupGrid
|
||||
id="itemGrid"
|
||||
translation="[ 96, 160 ]"
|
||||
itemComponentName="GridItem"
|
||||
numColumns="6"
|
||||
numRows="5"
|
||||
vertFocusAnimationStyle="fixed"
|
||||
itemSize="[ 290, 425 ]"
|
||||
itemSpacing="[ 0, 45 ]"
|
||||
drawFocusFeedback="false" />
|
||||
|
||||
<RowList opacity="0" id="genrelist" translation="[120, 160]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize = "[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
|
||||
<RowList opacity="0" id="genrelist" translation="[120, 160]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize="[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
|
||||
|
||||
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
|
||||
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
|
||||
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
|
||||
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
|
||||
<ItemGridOptions id="options" visible="false" />
|
||||
<Spinner id="spinner" translation="[900, 450]" />
|
||||
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
|
||||
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
</Animation>
|
||||
<Alpha id="AlphaMenu" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="HomeLibraryItem" type="string"/>
|
||||
<field id="HomeLibraryItem" type="string" />
|
||||
<field id="parentItem" type="node" onChange="loadInitialItems" />
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||
|
@ -39,9 +38,4 @@
|
|||
<field id="alphaActive" type="boolean" value="false" />
|
||||
<field id="jumpToItem" type="integer" value="" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
<script type="text/brightscript" uri="ItemGrid.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,5 +1,8 @@
|
|||
sub init()
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||
|
||||
sub init()
|
||||
m.log = log.Logger("ItemGridOptions")
|
||||
m.buttons = m.top.findNode("buttons")
|
||||
m.buttons.buttons = [tr("View"), tr("Sort"), tr("Filter")]
|
||||
m.buttons.selectedIndex = 1
|
||||
|
@ -53,6 +56,11 @@ sub hideChecklist()
|
|||
end sub
|
||||
|
||||
sub onFilterFocusChange()
|
||||
if not isFilterMenuDataValid()
|
||||
hideChecklist()
|
||||
return
|
||||
end if
|
||||
|
||||
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
|
||||
showChecklist()
|
||||
else
|
||||
|
@ -67,6 +75,18 @@ sub onFilterFocusChange()
|
|||
end if
|
||||
end sub
|
||||
|
||||
' Check if data for Filter Menu is valid
|
||||
function isFilterMenuDataValid() as boolean
|
||||
if not isValid(m.filterMenu) or not isValid(m.filterMenu.content) or not isValid(m.filterMenu.itemFocused)
|
||||
return false
|
||||
end if
|
||||
|
||||
if not isValid(m.filterMenu.content.getChild(m.filterMenu.itemFocused))
|
||||
return false
|
||||
end if
|
||||
|
||||
return true
|
||||
end function
|
||||
|
||||
sub optionsSet()
|
||||
' Views Tab
|
||||
|
@ -203,7 +223,7 @@ sub setHeartColor(color as string)
|
|||
end if
|
||||
end for
|
||||
catch e
|
||||
print e.number, e.message
|
||||
m.log.error("setHeartColor()", e.number, e.message)
|
||||
end try
|
||||
end sub
|
||||
|
||||
|
@ -227,7 +247,6 @@ sub saveFavoriteItemSelected(msg)
|
|||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
|
||||
if key = "down" or (key = "OK" and m.buttons.hasFocus())
|
||||
m.buttons.setFocus(false)
|
||||
m.menus[m.selectedItem].setFocus(true)
|
||||
|
@ -242,6 +261,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
|
||||
return true
|
||||
else if key = "right"
|
||||
if not isFilterMenuDataValid() then return false
|
||||
|
||||
if m.menus[m.selectedItem].isInFocusChain()
|
||||
' Handle Filter screen
|
||||
if m.selectedItem = 2
|
||||
|
@ -267,6 +288,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
return true
|
||||
end if
|
||||
else if key = "OK"
|
||||
|
||||
if m.menus[m.selectedItem].isInFocusChain()
|
||||
' Handle View Screen
|
||||
if m.selectedItem = 0
|
||||
|
@ -299,6 +321,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
|
||||
' Handle Filter screen
|
||||
if m.selectedItem = 2
|
||||
if not isFilterMenuDataValid() then return false
|
||||
' 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
|
||||
|
@ -362,5 +385,4 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
end if
|
||||
|
||||
return false
|
||||
|
||||
end function
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="ItemGridOptions" extends="Group">
|
||||
<children>
|
||||
<Rectangle width="1920" height="1080" color="#000000" opacity="0.75" />
|
||||
|
@ -9,11 +9,11 @@
|
|||
</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 id="viewMenu" itemSize="[600, 75]" 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 id="sortMenu" itemSize="[600, 75]" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="1" numRows="8" drawFocusFeedback="false">
|
||||
</RadiobuttonList>
|
||||
<RadiobuttonList id="filterMenu" checkOnSelect="false" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
|
||||
<RadiobuttonList id="filterMenu" itemSize="[600, 75]" checkOnSelect="false" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
|
||||
</RadiobuttonList>
|
||||
</Group>
|
||||
</LayoutGroup>
|
||||
|
@ -52,6 +52,4 @@
|
|||
<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>
|
||||
</component>
|
|
@ -1,8 +1,18 @@
|
|||
import "pkg:/source/api/Items.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||
import "pkg:/source/api/sdk.bs"
|
||||
|
||||
sub init()
|
||||
m.log = log.Logger("LoadItemsTask2")
|
||||
m.top.functionName = "loadItems"
|
||||
|
||||
m.top.limit = 60
|
||||
usersettingLimit = get_user_setting("itemgrid.Limit")
|
||||
usersettingLimit = m.global.session.user.settings["itemgrid.Limit"]
|
||||
|
||||
if usersettingLimit <> invalid
|
||||
m.top.limit = usersettingLimit
|
||||
|
@ -21,9 +31,9 @@ sub loadItems()
|
|||
end if
|
||||
|
||||
if m.top.ItemType = "LogoImage"
|
||||
logoImageExists = api_API().items.headimageurlbyname(m.top.itemId, "logo")
|
||||
logoImageExists = api.items.HeadImageURLByName(m.top.itemId, "logo")
|
||||
if logoImageExists
|
||||
m.top.content = [api_API().items.getimageurl(m.top.itemId, "logo", 0, { "maxHeight": 500, "maxWidth": 500, "quality": "90" })]
|
||||
m.top.content = [api.items.GetImageURL(m.top.itemId, "logo", 0, { "maxHeight": 500, "maxWidth": 500, "quality": "90" })]
|
||||
else
|
||||
m.top.content = []
|
||||
end if
|
||||
|
@ -95,26 +105,33 @@ sub loadItems()
|
|||
|
||||
if m.top.ItemType = "LiveTV"
|
||||
url = "LiveTv/Channels"
|
||||
params.append({ UserId: get_setting("active_user") })
|
||||
params.append({ UserId: m.global.session.user.id })
|
||||
else if m.top.view = "Networks"
|
||||
url = "Studios"
|
||||
params.append({ UserId: get_setting("active_user") })
|
||||
params.append({ UserId: m.global.session.user.id })
|
||||
else if m.top.view = "Genres"
|
||||
url = "Genres"
|
||||
params.append({ UserId: get_setting("active_user"), includeItemTypes: m.top.itemType })
|
||||
params.append({ UserId: m.global.session.user.id, includeItemTypes: m.top.itemType })
|
||||
else if m.top.ItemType = "MusicArtist"
|
||||
url = "Artists"
|
||||
params.append({
|
||||
UserId: get_setting("active_user"),
|
||||
UserId: m.global.session.user.id,
|
||||
Fields: "Genres"
|
||||
})
|
||||
params.IncludeItemTypes = "MusicAlbum,Audio"
|
||||
else if m.top.ItemType = "AlbumArtists"
|
||||
url = "Artists/AlbumArtists"
|
||||
params.append({
|
||||
UserId: m.global.session.user.id,
|
||||
Fields: "Genres"
|
||||
})
|
||||
params.IncludeItemTypes = "MusicAlbum,Audio"
|
||||
else if m.top.ItemType = "MusicAlbum"
|
||||
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
|
||||
url = Substitute("Users/{0}/Items/", m.global.session.user.id)
|
||||
params.append({ ImageTypeLimit: 1 })
|
||||
params.append({ EnableImageTypes: "Primary,Backdrop,Banner,Thumb" })
|
||||
else
|
||||
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
|
||||
url = Substitute("Users/{0}/Items/", m.global.session.user.id)
|
||||
end if
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
|
@ -151,7 +168,7 @@ sub loadItems()
|
|||
tmp = CreateObject("roSGNode", "ContentNode")
|
||||
tmp.title = item.name
|
||||
|
||||
genreData = api_API().users.getitemsbyquery(get_setting("active_user"), {
|
||||
genreData = api.users.GetItemsByQuery(m.global.session.user.id, {
|
||||
SortBy: "Random",
|
||||
SortOrder: "Ascending",
|
||||
IncludeItemTypes: m.top.itemType,
|
||||
|
@ -175,7 +192,7 @@ sub loadItems()
|
|||
row.type = "Folder"
|
||||
|
||||
if LCase(m.top.itemType) = "movie"
|
||||
genreItemImage = api_API().items.getimageurl(item.id)
|
||||
genreItemImage = api.items.GetImageURL(item.id)
|
||||
else
|
||||
genreItemImage = invalid
|
||||
row.posterURL = invalid
|
||||
|
@ -193,7 +210,7 @@ sub loadItems()
|
|||
row = tmp.createChild("SeriesData")
|
||||
end if
|
||||
|
||||
genreItemImage = api_API().items.getimageurl(genreItem.id)
|
||||
genreItemImage = api.items.GetImageURL(genreItem.id)
|
||||
row.title = genreItem.name
|
||||
row.FHDPOSTERURL = genreItemImage
|
||||
row.HDPOSTERURL = genreItemImage
|
||||
|
@ -208,7 +225,7 @@ sub loadItems()
|
|||
else if item.Type = "MusicAlbum"
|
||||
tmp = CreateObject("roSGNode", "MusicAlbumData")
|
||||
tmp.type = "MusicAlbum"
|
||||
if api_API().items.headimageurlbyname(item.id, "primary")
|
||||
if api.items.HeadImageURLByName(item.id, "primary")
|
||||
tmp.posterURL = ImageURL(item.id, "Primary")
|
||||
else
|
||||
tmp.posterURL = ImageURL(item.id, "backdrop")
|
||||
|
@ -218,17 +235,17 @@ sub loadItems()
|
|||
else if item.Type = "Audio"
|
||||
tmp = CreateObject("roSGNode", "MusicSongData")
|
||||
tmp.type = "Audio"
|
||||
tmp.image = api_API().items.getimageurl(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
|
||||
tmp.image = api.items.GetImageURL(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
|
||||
else if item.Type = "MusicGenre"
|
||||
tmp = CreateObject("roSGNode", "FolderData")
|
||||
tmp.title = item.name
|
||||
tmp.parentFolder = m.top.itemId
|
||||
tmp.json = item
|
||||
tmp.type = "Folder"
|
||||
tmp.posterUrl = api_API().items.getimageurl(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
|
||||
tmp.posterUrl = api.items.GetImageURL(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
|
||||
|
||||
else
|
||||
print "[LoadItems] Unknown Type: " item.Type
|
||||
m.log.warn("Unknown Type", item.Type)
|
||||
end if
|
||||
|
||||
if tmp <> invalid
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="LoadItemsTask2" extends="Task">
|
||||
<interface>
|
||||
|
@ -21,12 +21,4 @@
|
|||
<field id="totalRecordCount" type="int" value="-1" />
|
||||
<field id="content" type="array" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="LoadItemsTask2.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
</component>
|
|
@ -1,29 +1,62 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/api/Items.bs"
|
||||
import "pkg:/source/api/UserLibrary.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/api/userauth.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
|
||||
sub init()
|
||||
m.user = AboutMe()
|
||||
m.top.functionName = "loadItems"
|
||||
|
||||
m.top.limit = 60
|
||||
usersettingLimit = get_user_setting("itemgrid.Limit")
|
||||
|
||||
if isValid(usersettingLimit)
|
||||
m.top.limit = usersettingLimit
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub loadItems()
|
||||
m.top.content = [LoadItems_VideoPlayer(m.top.itemId)]
|
||||
' Reset intro tracker in case task gets reused
|
||||
m.top.isIntro = false
|
||||
|
||||
' Only show preroll once per queue
|
||||
if m.global.queueManager.callFunc("isPrerollActive")
|
||||
' Prerolls not allowed if we're resuming video
|
||||
if m.global.queueManager.callFunc("getCurrentItem").startingPoint = 0
|
||||
preRoll = GetIntroVideos(m.top.itemId)
|
||||
if isValid(preRoll) and preRoll.TotalRecordCount > 0 and isValid(preRoll.items[0])
|
||||
' If an error is thrown in the Intros plugin, instead of passing the error they pass the entire rick roll music video.
|
||||
' Bypass the music video and treat it as an error message
|
||||
if lcase(preRoll.items[0].name) <> "rick roll'd"
|
||||
m.global.queueManager.callFunc("push", m.global.queueManager.callFunc("getCurrentItem"))
|
||||
m.top.itemId = preRoll.items[0].id
|
||||
m.global.queueManager.callFunc("setPrerollStatus", false)
|
||||
m.top.isIntro = true
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
if m.top.selectedAudioStreamIndex = 0
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
if isValid(currentItem) and isValid(currentItem.json)
|
||||
m.top.selectedAudioStreamIndex = FindPreferredAudioStream(currentItem.json.MediaStreams)
|
||||
end if
|
||||
end if
|
||||
|
||||
id = m.top.itemId
|
||||
mediaSourceId = invalid
|
||||
audio_stream_idx = m.top.selectedAudioStreamIndex
|
||||
subtitle_idx = m.top.selectedSubtitleIndex
|
||||
forceTranscoding = false
|
||||
|
||||
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)]
|
||||
end sub
|
||||
|
||||
function LoadItems_VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle_idx = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
|
||||
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) as dynamic
|
||||
|
||||
video = {}
|
||||
video.id = id
|
||||
video.content = createObject("RoSGNode", "ContentNode")
|
||||
|
||||
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro, allowResumeDialog)
|
||||
|
||||
if video.errorMsg = "introaborted"
|
||||
return video
|
||||
end if
|
||||
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)
|
||||
|
||||
if video.content = invalid
|
||||
return invalid
|
||||
|
@ -32,11 +65,12 @@ function LoadItems_VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1
|
|||
return video
|
||||
end function
|
||||
|
||||
sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
|
||||
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean)
|
||||
|
||||
meta = ItemMetaData(video.id)
|
||||
|
||||
if not isValid(meta)
|
||||
video.errorMsg = "Error loading metadata"
|
||||
video.content = invalid
|
||||
return
|
||||
end if
|
||||
|
@ -47,45 +81,28 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
video.content.contenttype = "episode"
|
||||
end if
|
||||
|
||||
video.chapters = meta.json.Chapters
|
||||
video.content.title = meta.title
|
||||
video.showID = meta.showID
|
||||
|
||||
if playbackPosition = -1
|
||||
playbackPosition = meta.json.UserData.PlaybackPositionTicks
|
||||
if allowResumeDialog
|
||||
if playbackPosition > 0
|
||||
dialogResult = startPlayBackOver(playbackPosition)
|
||||
|
||||
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
|
||||
if dialogResult.indexselected = -1
|
||||
'User pressed back, return invalid and don't load video
|
||||
video.content = invalid
|
||||
return
|
||||
else if dialogResult.indexselected = 1
|
||||
'Start Over selected, change position to 0
|
||||
playbackPosition = 0
|
||||
end if
|
||||
end if
|
||||
user = AboutMe()
|
||||
if user.Configuration.EnableNextEpisodeAutoPlay
|
||||
if LCase(m.top.itemType) = "episode"
|
||||
addNextEpisodesToQueue(video.showID)
|
||||
end if
|
||||
end if
|
||||
|
||||
' For phase 1 of playlist support, we don't support intros yet
|
||||
showIntro = false
|
||||
playbackPosition = 0!
|
||||
|
||||
' Don't attempt to play an intro for an intro video
|
||||
if showIntro
|
||||
' Do not play intros when resuming playback
|
||||
if playbackPosition = 0
|
||||
if not PlayIntroVideo(video.id, audio_stream_idx)
|
||||
video.errorMsg = "introaborted"
|
||||
return
|
||||
end if
|
||||
end if
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
|
||||
if isValid(currentItem) and isValid(currentItem.startingPoint)
|
||||
playbackPosition = currentItem.startingPoint
|
||||
end if
|
||||
|
||||
' PlayStart requires the time to be in seconds
|
||||
video.content.PlayStart = int(playbackPosition / 10000000)
|
||||
|
||||
|
||||
if not isValid(mediaSourceId) then mediaSourceId = video.id
|
||||
if meta.live then mediaSourceId = ""
|
||||
|
||||
|
@ -95,6 +112,7 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
video.audioIndex = audio_stream_idx
|
||||
|
||||
if not isValid(m.playbackInfo)
|
||||
video.errorMsg = "Error loading playback info"
|
||||
video.content = invalid
|
||||
return
|
||||
end if
|
||||
|
@ -124,7 +142,6 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
|
||||
|
||||
' '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
|
||||
|
||||
|
@ -135,8 +152,8 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
' transcode is that the Encoding Level is not supported, then try to direct play but silently
|
||||
' fall back to the transcode if that fails.
|
||||
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")
|
||||
tryDirectPlay = m.global.session.user.settings["playback.tryDirect.h264ProfileLevel"] and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
|
||||
tryDirectPlay = tryDirectPlay or (m.global.session.user.settings["playback.tryDirect.hevcProfileLevel"] and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
|
||||
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"
|
||||
|
@ -147,12 +164,13 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
end if
|
||||
|
||||
if video.directPlaySupported
|
||||
addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||
video.isTranscoded = false
|
||||
addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||
else
|
||||
if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid
|
||||
' If server does not provide a transcode URL, display a message to the user
|
||||
m.global.sceneManager.callFunc("userMessage", tr("Error Getting Playback Information"), tr("An error was encountered while playing this item. Server did not provide required transcoding data."))
|
||||
video.errorMsg = "Error getting playback information"
|
||||
video.content = invalid
|
||||
return
|
||||
end if
|
||||
|
@ -162,28 +180,21 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
video.isTranscoded = true
|
||||
end if
|
||||
|
||||
video.content.setCertificatesFile("common:/certs/ca-bundle.crt")
|
||||
setCertificateAuthority(video.content)
|
||||
video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based
|
||||
|
||||
' Perform relevant setup work for selected subtitle, and return the index of the subtitle
|
||||
' is enabled/will be enabled, indexed on the provided list of subtitles
|
||||
video.SelectedSubtitle = setupSubtitle(video, video.Subtitles, subtitle_idx)
|
||||
video.SelectedSubtitle = subtitle_idx
|
||||
|
||||
if not fully_external
|
||||
video.content = authorize_request(video.content)
|
||||
video.content = authRequest(video.content)
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
||||
sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
||||
if protocol <> "file"
|
||||
uriRegex = CreateObject("roRegex", "^(.*:)//([A-Za-z0-9\-\.]+)(:[0-9]+)?(.*)$", "")
|
||||
uri = uriRegex.Match(m.playbackInfo.MediaSources[0].Path)
|
||||
' proto $1, host $2, port $3, the-rest $4
|
||||
localhost = CreateObject("roRegex", "^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$", "i")
|
||||
' https://stackoverflow.com/questions/8426171/what-regex-will-match-all-loopback-addresses
|
||||
if localhost.isMatch(uri[2])
|
||||
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
||||
if isLocalhost(uri[2])
|
||||
' if the domain of the URI is local to the server,
|
||||
' create a new URI by appending the received path to the server URL
|
||||
' later we will substitute the users provided URL for this case
|
||||
|
@ -192,15 +203,13 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|||
fully_external = true
|
||||
video.content.url = m.playbackInfo.MediaSources[0].Path
|
||||
end if
|
||||
else:
|
||||
params = {}
|
||||
|
||||
params.append({
|
||||
else
|
||||
params = {
|
||||
"Static": "true",
|
||||
"Container": video.container,
|
||||
"PlaySessionId": video.PlaySessionId,
|
||||
"AudioStreamIndex": audio_stream_idx
|
||||
})
|
||||
}
|
||||
|
||||
if mediaSourceId <> ""
|
||||
params.MediaSourceId = mediaSourceId
|
||||
|
@ -212,18 +221,19 @@ end sub
|
|||
|
||||
sub addSubtitlesToVideo(video, meta)
|
||||
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
|
||||
if get_user_setting("playback.subs.onlytext") = "true"
|
||||
safesubs = []
|
||||
for each subtitle in subtitles["all"]
|
||||
if subtitle["IsTextSubtitleStream"]
|
||||
safesubs.push(subtitle)
|
||||
end if
|
||||
end for
|
||||
video.Subtitles = safesubs
|
||||
else
|
||||
video.Subtitles = subtitles["all"]
|
||||
safesubs = subtitles["all"]
|
||||
subtitleTracks = []
|
||||
|
||||
if m.global.session.user.settings["playback.subs.onlytext"] = true
|
||||
safesubs = subtitles["text"]
|
||||
end if
|
||||
video.content.SubtitleTracks = subtitles["text"]
|
||||
|
||||
for each subtitle in safesubs
|
||||
subtitleTracks.push(subtitle.track)
|
||||
end for
|
||||
|
||||
video.content.SubtitleTracks = subtitleTracks
|
||||
video.fullSubtitleData = safesubs
|
||||
end sub
|
||||
|
||||
|
||||
|
@ -242,26 +252,6 @@ function getTranscodeReasons(url as string) as object
|
|||
return []
|
||||
end function
|
||||
|
||||
'Opens dialog asking user if they want to resume video or start playback over only on the home screen
|
||||
function startPlayBackOver(time as longinteger)
|
||||
|
||||
' If we're inside a play queue, start the episode from the beginning
|
||||
if m.global.queueManager.callFunc("getCount") > 1 then return { indexselected: 1 }
|
||||
|
||||
resumeData = [
|
||||
"Resume playing at " + ticksToHuman(time) + ".",
|
||||
"Start over from the beginning."
|
||||
]
|
||||
|
||||
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), ["Choose an option"], resumeData)
|
||||
|
||||
while not isValid(m.global.sceneManager.returnData)
|
||||
|
||||
end while
|
||||
|
||||
return m.global.sceneManager.returnData
|
||||
end function
|
||||
|
||||
function directPlaySupported(meta as object) as boolean
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
|
||||
|
@ -304,308 +294,53 @@ function getContainerType(meta as object) as string
|
|||
return container
|
||||
end function
|
||||
|
||||
function getAudioFormat(meta as object) as string
|
||||
' Determine the codec of the audio file source
|
||||
if meta.json.mediaSources = invalid then return ""
|
||||
' Add next episodes to the playback queue
|
||||
sub addNextEpisodesToQueue(showID)
|
||||
' Don't queue next episodes if we already have a playback queue
|
||||
maxQueueCount = 1
|
||||
|
||||
audioInfo = getAudioInfo(meta)
|
||||
if audioInfo.count() = 0 or audioInfo[0].codec = invalid then return ""
|
||||
return audioInfo[0].codec
|
||||
end function
|
||||
if m.top.isIntro
|
||||
maxQueueCount = 2
|
||||
end if
|
||||
|
||||
function getAudioInfo(meta as object) as object
|
||||
' Return audio metadata for a given stream
|
||||
results = []
|
||||
for each source in meta.json.mediaSources[0].mediaStreams
|
||||
if source["type"] = "Audio"
|
||||
results.push(source)
|
||||
end if
|
||||
end for
|
||||
return results
|
||||
end function
|
||||
if m.global.queueManager.callFunc("getCount") > maxQueueCount then return
|
||||
|
||||
sub autoPlayNextEpisode(videoID as string, showID as string)
|
||||
' use web client setting
|
||||
if m.user.Configuration.EnableNextEpisodeAutoPlay
|
||||
' query API for next episode ID
|
||||
url = Substitute("Shows/{0}/Episodes", showID)
|
||||
urlParams = { "UserId": get_setting("active_user") }
|
||||
urlParams.Append({ "StartItemId": videoID })
|
||||
urlParams.Append({ "Limit": 2 })
|
||||
resp = APIRequest(url, urlParams)
|
||||
data = getJson(resp)
|
||||
videoID = m.top.itemId
|
||||
|
||||
if isValid(data) and data.Items.Count() = 2
|
||||
' setup new video node
|
||||
nextVideo = invalid
|
||||
' remove last videoplayer scene
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
if isValid(nextVideo)
|
||||
m.global.sceneManager.callFunc("pushScene", nextVideo)
|
||||
else
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
' If first item is an intro video, use the next item in the queue
|
||||
if m.top.isIntro
|
||||
currentVideo = m.global.queueManager.callFunc("getItemByIndex", 1)
|
||||
|
||||
if isValid(currentVideo) and isValid(currentVideo.id)
|
||||
videoID = currentVideo.id
|
||||
|
||||
' Override showID value since it's for the intro video
|
||||
meta = ItemMetaData(videoID)
|
||||
if isValid(meta)
|
||||
showID = meta.showID
|
||||
end if
|
||||
else
|
||||
' can't play next episode
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
end if
|
||||
else
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
end if
|
||||
|
||||
url = Substitute("Shows/{0}/Episodes", showID)
|
||||
urlParams = { "UserId": m.global.session.user.id }
|
||||
urlParams.Append({ "StartItemId": videoID })
|
||||
urlParams.Append({ "Limit": 50 })
|
||||
resp = APIRequest(url, urlParams)
|
||||
data = getJson(resp)
|
||||
|
||||
if isValid(data) and data.Items.Count() > 1
|
||||
for i = 1 to data.Items.Count() - 1
|
||||
m.global.queueManager.callFunc("push", data.Items[i])
|
||||
end for
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Returns an array of playback info to be displayed during playback.
|
||||
' In the future, with a custom playback info view, we can return an associated array.
|
||||
function GetPlaybackInfo()
|
||||
sessions = api_API().sessions.get()
|
||||
if isValid(sessions) and sessions.Count() > 0
|
||||
return GetTranscodingStats(sessions[0])
|
||||
end if
|
||||
|
||||
errMsg = tr("Unable to get playback information")
|
||||
return [errMsg]
|
||||
end function
|
||||
|
||||
function GetTranscodingStats(session)
|
||||
sessionStats = []
|
||||
|
||||
if isValid(session.TranscodingInfo) and session.TranscodingInfo.Count() > 0
|
||||
transcodingReasons = session.TranscodingInfo.TranscodeReasons
|
||||
videoCodec = session.TranscodingInfo.VideoCodec
|
||||
audioCodec = session.TranscodingInfo.AudioCodec
|
||||
totalBitrate = session.TranscodingInfo.Bitrate
|
||||
audioChannels = session.TranscodingInfo.AudioChannels
|
||||
|
||||
if isValid(transcodingReasons) and transcodingReasons.Count() > 0
|
||||
sessionStats.push("** " + tr("Transcoding Information") + " **")
|
||||
for each item in transcodingReasons
|
||||
sessionStats.push(tr("Reason") + ": " + item)
|
||||
end for
|
||||
end if
|
||||
|
||||
if isValid(videoCodec)
|
||||
data = tr("Video Codec") + ": " + videoCodec
|
||||
if session.TranscodingInfo.IsVideoDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
|
||||
if isValid(audioCodec)
|
||||
data = tr("Audio Codec") + ": " + audioCodec
|
||||
if session.TranscodingInfo.IsAudioDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
|
||||
if isValid(totalBitrate)
|
||||
data = tr("Total Bitrate") + ": " + getDisplayBitrate(totalBitrate)
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
|
||||
if isValid(audioChannels)
|
||||
data = tr("Audio Channels") + ": " + Str(audioChannels)
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
end if
|
||||
|
||||
if havePlaybackInfo()
|
||||
stream = m.playbackInfo.mediaSources[0].MediaStreams[0]
|
||||
sessionStats.push("** " + tr("Stream Information") + " **")
|
||||
if isValid(stream.Container)
|
||||
data = tr("Container") + ": " + stream.Container
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.Size)
|
||||
data = tr("Size") + ": " + stream.Size
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.BitRate)
|
||||
data = tr("Bit Rate") + ": " + getDisplayBitrate(stream.BitRate)
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.Codec)
|
||||
data = tr("Codec") + ": " + stream.Codec
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.CodecTag)
|
||||
data = tr("Codec Tag") + ": " + stream.CodecTag
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.VideoRangeType)
|
||||
data = tr("Video range type") + ": " + stream.VideoRangeType
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.PixelFormat)
|
||||
data = tr("Pixel format") + ": " + stream.PixelFormat
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.Width) and isValid(stream.Height)
|
||||
data = tr("WxH") + ": " + Str(stream.Width) + " x " + Str(stream.Height)
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
if isValid(stream.Level)
|
||||
data = tr("Level") + ": " + Str(stream.Level)
|
||||
sessionStats.push(data)
|
||||
end if
|
||||
end if
|
||||
|
||||
return sessionStats
|
||||
end function
|
||||
|
||||
function havePlaybackInfo()
|
||||
if not isValid(m.playbackInfo)
|
||||
return false
|
||||
end if
|
||||
|
||||
if not isValid(m.playbackInfo.mediaSources)
|
||||
return false
|
||||
end if
|
||||
|
||||
if m.playbackInfo.mediaSources.Count() <= 0
|
||||
return false
|
||||
end if
|
||||
|
||||
if not isValid(m.playbackInfo.mediaSources[0].MediaStreams)
|
||||
return false
|
||||
end if
|
||||
|
||||
if m.playbackInfo.mediaSources[0].MediaStreams.Count() <= 0
|
||||
return false
|
||||
end if
|
||||
|
||||
return true
|
||||
end function
|
||||
|
||||
function getDisplayBitrate(bitrate)
|
||||
if bitrate > 1000000
|
||||
return Str(Fix(bitrate / 1000000)) + " Mbps"
|
||||
else
|
||||
return Str(Fix(bitrate / 1000)) + " Kbps"
|
||||
end if
|
||||
end function
|
||||
|
||||
' Roku translates the info provided in subtitleTracks into availableSubtitleTracks
|
||||
' Including ignoring tracks, if they are not understood, thus making indexing unpredictable.
|
||||
' This function translates between our internel selected subtitle index
|
||||
' and the corresponding index in availableSubtitleTracks.
|
||||
function availSubtitleTrackIdx(video, sub_idx) as integer
|
||||
url = video.Subtitles[sub_idx].Track.TrackName
|
||||
idx = 0
|
||||
for each availTrack in video.availableSubtitleTracks
|
||||
' The TrackName must contain the URL we supplied originally, though
|
||||
' Roku mangles the name a bit, so we check if the URL is a substring, rather
|
||||
' than strict equality
|
||||
if Instr(1, availTrack.TrackName, url)
|
||||
return idx
|
||||
end if
|
||||
idx = idx + 1
|
||||
end for
|
||||
return -1
|
||||
end function
|
||||
|
||||
' Identify the default subtitle track for a given video id
|
||||
' returns the server-side track index for the appriate subtitle
|
||||
function defaultSubtitleTrackFromVid(video_id) as integer
|
||||
meta = ItemMetaData(video_id)
|
||||
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
|
||||
return default_text_subs
|
||||
else
|
||||
if get_user_setting("playback.subs.onlytext") = "false"
|
||||
return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text
|
||||
else
|
||||
return -1
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
' No valid mediaSources (i.e. LiveTV)
|
||||
return -1
|
||||
end function
|
||||
|
||||
|
||||
' Identify the default subtitle track
|
||||
' if "requires_text" is true, only return a track if it is textual
|
||||
' This allows forcing text subs, since roku requires transcoding of non-text subs
|
||||
' returns the server-side track index for the appriate subtitle
|
||||
function defaultSubtitleTrack(sorted_subtitles, require_text = false) as integer
|
||||
if m.user.Configuration.SubtitleMode = "None"
|
||||
return -1 ' No subtitles desired: select none
|
||||
end if
|
||||
|
||||
for each item in sorted_subtitles
|
||||
' Only auto-select subtitle if language matches preference
|
||||
languageMatch = (m.user.Configuration.SubtitleLanguagePreference = item.Track.Language)
|
||||
' Ensure textuality of subtitle matches preferenced passed as arg
|
||||
matchTextReq = ((require_text and item.IsTextSubtitleStream) or not require_text)
|
||||
if languageMatch and matchTextReq
|
||||
if m.user.Configuration.SubtitleMode = "Default" and (item.isForced or item.IsDefault or item.IsExternal)
|
||||
return item.Index ' Finds first forced, or default, or external subs in sorted list
|
||||
else if m.user.Configuration.SubtitleMode = "Always" and not item.IsForced
|
||||
return item.Index ' Select the first non-forced subtitle option in the sorted list
|
||||
else if m.user.Configuration.SubtitleMode = "OnlyForced" and item.IsForced
|
||||
return item.Index ' Select the first forced subtitle option in the sorted list
|
||||
else if m.user.Configuration.SubtitlePlaybackMode = "Smart" and (item.isForced or item.IsDefault or item.IsExternal)
|
||||
' Simplified "Smart" logic here mimics Default (as that is fallback behavior normally)
|
||||
' Avoids detecting preferred audio language (as is utilized in main client)
|
||||
return item.Index
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
return -1 ' Keep current default behavior of "None", if no correct subtitle is identified
|
||||
end function
|
||||
|
||||
' Given a set of subtitles, and a subtitle index (the index on the server, not in the list provided)
|
||||
' this will set all relevant settings for roku (mainly closed captions) and return the index of the
|
||||
' subtitle track specified, but indexed based on the provided list of subtitles
|
||||
function setupSubtitle(video, subtitles, subtitle_idx = -1) as integer
|
||||
if subtitle_idx = -1
|
||||
' If we are not using text-based subtitles, turn them off
|
||||
video.globalCaptionMode = "Off"
|
||||
return -1
|
||||
end if
|
||||
|
||||
' Translate the raw index to one relative to the provided list
|
||||
subtitleSelIdx = getSubtitleSelIdxFromSubIdx(subtitles, subtitle_idx)
|
||||
|
||||
selectedSubtitle = subtitles[subtitleSelIdx]
|
||||
|
||||
if selectedSubtitle.IsEncoded
|
||||
' With encoded subtitles, turn off captions
|
||||
video.globalCaptionMode = "Off"
|
||||
else
|
||||
' If this is a text-based subtitle, set relevant settings for roku captions
|
||||
video.globalCaptionMode = "On"
|
||||
video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, subtitleSelIdx)].TrackName
|
||||
end if
|
||||
|
||||
return subtitleSelIdx
|
||||
|
||||
end function
|
||||
|
||||
' The subtitle index on the server differs from the index we track locally
|
||||
' This function converts the former into the latter
|
||||
function getSubtitleSelIdxFromSubIdx(subtitles, sub_idx) as integer
|
||||
selIdx = 0
|
||||
if sub_idx = -1 then return -1
|
||||
for each item in subtitles
|
||||
if item.Index = sub_idx
|
||||
return selIdx
|
||||
end if
|
||||
selIdx = selIdx + 1
|
||||
end for
|
||||
return -1
|
||||
end function
|
||||
|
||||
'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top
|
||||
function sortSubtitles(id as string, MediaStreams)
|
||||
m.user = AboutMe()
|
||||
tracks = { "forced": [], "default": [], "normal": [] }
|
||||
tracks = { "forced": [], "default": [], "normal": [], "text": [] }
|
||||
'Too many args for using substitute
|
||||
prefered_lang = m.user.Configuration.SubtitleLanguagePreference
|
||||
prefered_lang = m.global.session.user.configuration.SubtitleLanguagePreference
|
||||
for each stream in MediaStreams
|
||||
if stream.type = "Subtitle"
|
||||
|
||||
|
@ -627,6 +362,8 @@ function sortSubtitles(id as string, MediaStreams)
|
|||
trackType = "forced"
|
||||
else if stream.IsDefault
|
||||
trackType = "default"
|
||||
else if stream.IsTextSubtitleStream
|
||||
trackType = "text"
|
||||
else
|
||||
trackType = "normal"
|
||||
end if
|
||||
|
@ -640,14 +377,41 @@ function sortSubtitles(id as string, MediaStreams)
|
|||
|
||||
tracks["default"].append(tracks["normal"])
|
||||
tracks["forced"].append(tracks["default"])
|
||||
tracks["forced"].append(tracks["text"])
|
||||
|
||||
textTracks = []
|
||||
for i = 0 to tracks["forced"].count() - 1
|
||||
if tracks["forced"][i].IsTextSubtitleStream
|
||||
textTracks.push(tracks["forced"][i].Track)
|
||||
end if
|
||||
end for
|
||||
return { "all": tracks["forced"], "text": textTracks }
|
||||
return { "all": tracks["forced"], "text": tracks["text"] }
|
||||
end function
|
||||
|
||||
function FindPreferredAudioStream(streams as dynamic) as integer
|
||||
preferredLanguage = m.user.Configuration.AudioLanguagePreference
|
||||
playDefault = m.user.Configuration.PlayDefaultAudioTrack
|
||||
|
||||
if playDefault <> invalid and playDefault = true
|
||||
return 1
|
||||
end if
|
||||
|
||||
' Do we already have the MediaStreams or not?
|
||||
if streams = invalid
|
||||
url = Substitute("Users/{0}/Items/{1}", m.user.id, m.top.itemId)
|
||||
resp = APIRequest(url)
|
||||
jsonResponse = getJson(resp)
|
||||
|
||||
if jsonResponse = invalid or jsonResponse.MediaStreams = invalid then return 1
|
||||
|
||||
streams = jsonResponse.MediaStreams
|
||||
end if
|
||||
|
||||
if preferredLanguage <> invalid
|
||||
for i = 0 to streams.Count() - 1
|
||||
if LCase(streams[i].Type) = "audio"
|
||||
if streams[i].Language <> invalid and LCase(streams[i].Language) = LCase(preferredLanguage)
|
||||
return i
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
return 1
|
||||
end function
|
||||
|
||||
function getSubtitleLanguages()
|
||||
|
@ -1143,122 +907,3 @@ function getSubtitleLanguages()
|
|||
"zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
|
||||
}
|
||||
end function
|
||||
|
||||
function CreateSeasonDetailsGroup(series, season)
|
||||
group = CreateObject("roSGNode", "TVEpisodes")
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
group.seasonData = ItemMetaData(season.id).json
|
||||
group.objects = TVEpisodes(series.id, season.id)
|
||||
|
||||
group.observeField("episodeSelected", m.port)
|
||||
group.observeField("quickPlayNode", m.port)
|
||||
|
||||
return group
|
||||
end function
|
||||
|
||||
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)
|
||||
|
||||
if introVideos = invalid then return true
|
||||
|
||||
if introVideos.TotalRecordCount > 0
|
||||
' Bypass joke pre-roll
|
||||
if lcase(introVideos.items[0].name) = "rick roll'd" then return true
|
||||
|
||||
introVideo = LoadItems_VideoPlayer(introVideos.items[0].id, introVideos.items[0].id, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), false, false)
|
||||
|
||||
port = CreateObject("roMessagePort")
|
||||
introVideo.observeField("state", port)
|
||||
m.global.sceneManager.callFunc("pushScene", introVideo)
|
||||
introPlaying = true
|
||||
|
||||
while introPlaying
|
||||
msg = wait(0, port)
|
||||
if type(msg) = "roSGNodeEvent"
|
||||
if msg.GetData() = "finished"
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
introPlaying = false
|
||||
else if msg.GetData() = "stopped"
|
||||
introPlaying = false
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end while
|
||||
end if
|
||||
end if
|
||||
return true
|
||||
end function
|
||||
|
||||
function CreateMovieDetailsGroup(movie)
|
||||
group = CreateObject("roSGNode", "MovieDetails")
|
||||
group.overhangTitle = movie.title
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
movieMetaData = ItemMetaData(movie.id)
|
||||
group.itemContent = movieMetaData
|
||||
group.trailerAvailable = false
|
||||
|
||||
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
|
||||
|
||||
buttons = group.findNode("buttons")
|
||||
for each b in buttons.getChildren(-1, 0)
|
||||
b.observeField("buttonSelected", m.port)
|
||||
end for
|
||||
|
||||
extras = group.findNode("extrasGrid")
|
||||
extras.observeField("selectedItem", m.port)
|
||||
extras.callFunc("loadParts", movieMetaData.json)
|
||||
|
||||
return group
|
||||
end function
|
||||
|
||||
function CreateSeriesDetailsGroup(series)
|
||||
' Get season data early in the function so we can check number of seasons.
|
||||
seasonData = TVSeasons(series.id)
|
||||
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
|
||||
if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1
|
||||
return CreateSeasonDetailsGroupByID(series.id, seasonData.Items[0].id)
|
||||
end if
|
||||
group = CreateObject("roSGNode", "TVShowDetails")
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
group.itemContent = ItemMetaData(series.id)
|
||||
group.seasonData = seasonData ' Re-use variable from beginning of function
|
||||
|
||||
group.observeField("seasonSelected", m.port)
|
||||
|
||||
extras = group.findNode("extrasGrid")
|
||||
extras.observeField("selectedItem", m.port)
|
||||
extras.callFunc("loadParts", group.itemcontent.json)
|
||||
|
||||
return group
|
||||
end function
|
||||
|
||||
function CreateSeasonDetailsGroupByID(seriesID, seasonID)
|
||||
group = CreateObject("roSGNode", "TVEpisodes")
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
group.seasonData = ItemMetaData(seasonID).json
|
||||
group.objects = TVEpisodes(seriesID, seasonID)
|
||||
|
||||
group.observeField("episodeSelected", m.port)
|
||||
group.observeField("quickPlayNode", m.port)
|
||||
|
||||
return group
|
||||
end function
|
|
@ -1,11 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="LoadVideoContentTask" extends="Task">
|
||||
<interface>
|
||||
<field id="itemId" type="string" />
|
||||
<field id="selectedAudioStreamIndex" type="integer" value="0" />
|
||||
<field id="selectedSubtitleIndex" type="integer" value="-1" />
|
||||
<field id="isIntro" type="boolean" />
|
||||
<field id="startIndex" type="integer" value="0" />
|
||||
<field id="itemType" type="string" value="" />
|
||||
<field id="limit" type="integer" value="60" />
|
||||
<field id="metadata" type="assocarray" />
|
||||
<field id="sortField" type="string" value="SortName" />
|
||||
<field id="sortAscending" type="boolean" value="true" />
|
||||
|
@ -20,14 +22,4 @@
|
|||
<field id="totalRecordCount" type="int" value="-1" />
|
||||
<field id="content" type="array" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="LoadVideoContentTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/UserLibrary.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/userauth.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
</component>
|
|
@ -1,3 +1,9 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
|
||||
sub setupNodes()
|
||||
m.options = m.top.findNode("options")
|
||||
m.itemGrid = m.top.findNode("itemGrid")
|
||||
|
@ -11,7 +17,6 @@ sub setupNodes()
|
|||
m.selectedMovieOfficialRating = m.top.findNode("selectedMovieOfficialRating")
|
||||
m.movieLogo = m.top.findNode("movieLogo")
|
||||
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
|
||||
m.spinner = m.top.findNode("spinner")
|
||||
m.Alpha = m.top.findNode("AlphaMenu")
|
||||
m.AlphaSelected = m.top.findNode("AlphaSelected")
|
||||
m.micButton = m.top.findNode("micButton")
|
||||
|
@ -30,7 +35,7 @@ sub init()
|
|||
|
||||
m.overhang.isVisible = false
|
||||
|
||||
m.showItemCount = get_user_setting("itemgrid.showItemCount") = "true"
|
||||
m.showItemCount = m.global.session.user.settings["itemgrid.showItemCount"]
|
||||
|
||||
m.swapAnimation.observeField("state", "swapDone")
|
||||
|
||||
|
@ -77,16 +82,11 @@ sub init()
|
|||
'set inital counts for overhang before content is loaded.
|
||||
m.loadItemsTask.totalRecordCount = 0
|
||||
|
||||
m.spinner.visible = true
|
||||
|
||||
'Get reset folder setting
|
||||
m.resetGrid = get_user_setting("itemgrid.reset") = "true"
|
||||
|
||||
'Check if device has voice remote
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
|
||||
|
||||
'Hide voice search if device does not have voice remote
|
||||
if devinfo.HasFeature("voice_remote") = false
|
||||
if m.global.device.hasVoiceRemote = false
|
||||
m.micButton.visible = false
|
||||
m.micButtonText.visible = false
|
||||
end if
|
||||
|
@ -114,7 +114,7 @@ end sub
|
|||
'Load initial set of Data
|
||||
sub loadInitialItems()
|
||||
m.loadItemsTask.control = "stop"
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner(false)
|
||||
|
||||
if m.top.parentItem.json.Type = "CollectionFolder"
|
||||
m.top.HomeLibraryItem = m.top.parentItem.Id
|
||||
|
@ -126,30 +126,25 @@ sub loadInitialItems()
|
|||
SetBackground("")
|
||||
end if
|
||||
|
||||
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")
|
||||
m.sortField = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortField"]
|
||||
m.filter = m.global.session.user.settings["display." + m.top.parentItem.Id + ".filter"]
|
||||
m.filterOptions = m.global.session.user.settings["display." + m.top.parentItem.Id + ".filterOptions"]
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
m.sortAscending = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortAscending"]
|
||||
|
||||
' If user has not set a preferred view for this folder, check if they've set a default view
|
||||
if not isValid(m.view)
|
||||
m.view = get_user_setting("itemgrid.movieDefaultView")
|
||||
m.view = m.global.session.user.settings["itemgrid.movieDefaultView"]
|
||||
end if
|
||||
|
||||
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"
|
||||
if not isValid(m.sortAscending) then m.sortAscending = true
|
||||
|
||||
m.filterOptions = ParseJson(m.filterOptions)
|
||||
|
||||
if sortAscendingStr = invalid or sortAscendingStr = "true"
|
||||
m.sortAscending = true
|
||||
else
|
||||
m.sortAscending = false
|
||||
end if
|
||||
|
||||
if m.top.parentItem.json.type = "Studio"
|
||||
m.loadItemsTask.studioIds = m.top.parentItem.id
|
||||
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
|
||||
|
@ -203,7 +198,7 @@ sub loadInitialItems()
|
|||
m.itemGrid.numRows = "3"
|
||||
m.selectedMovieOverview.visible = false
|
||||
m.infoGroup.visible = false
|
||||
m.top.showItemTitles = get_user_setting("itemgrid.gridTitles")
|
||||
m.top.showItemTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
||||
if LCase(m.top.showItemTitles) = "hidealways"
|
||||
m.itemGrid.itemSize = "[230, 315]"
|
||||
m.itemGrid.rowHeights = "[315]"
|
||||
|
@ -221,12 +216,11 @@ sub loadInitialItems()
|
|||
end if
|
||||
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.control = "RUN"
|
||||
|
||||
m.getFiltersTask.observeField("filters", "FilterDataLoaded")
|
||||
m.getFiltersTask.params = {
|
||||
userid: get_setting("active_user"),
|
||||
userid: m.global.session.user.id,
|
||||
parentid: m.top.parentItem.Id,
|
||||
includeitemtypes: "Movie"
|
||||
}
|
||||
|
@ -438,7 +432,7 @@ sub ItemDataLoaded(msg)
|
|||
m.genreList.setFocus(true)
|
||||
|
||||
m.loading = false
|
||||
m.spinner.visible = false
|
||||
stopLoadingSpinner()
|
||||
' Return focus to options menu if it was opened while library was loading
|
||||
if m.options.visible
|
||||
m.options.setFocus(true)
|
||||
|
@ -488,7 +482,7 @@ sub ItemDataLoaded(msg)
|
|||
m.emptyText.visible = true
|
||||
end if
|
||||
|
||||
m.spinner.visible = false
|
||||
stopLoadingSpinner()
|
||||
' Return focus to options menu if it was opened while library was loading
|
||||
if m.options.visible
|
||||
m.options.setFocus(true)
|
||||
|
@ -693,8 +687,9 @@ end sub
|
|||
'
|
||||
'Load next set of items
|
||||
sub loadMoreData()
|
||||
m.spinner.visible = true
|
||||
if m.Loading = true then return
|
||||
|
||||
startLoadingSpinner(false)
|
||||
m.Loading = true
|
||||
m.loadItemsTask.startIndex = m.loadedItems
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
|
@ -710,7 +705,12 @@ end sub
|
|||
'
|
||||
'Returns Focused Item
|
||||
function getItemFocused()
|
||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||
else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused)
|
||||
return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1])
|
||||
end if
|
||||
return invalid
|
||||
end function
|
||||
|
||||
'
|
||||
|
@ -733,7 +733,6 @@ sub onItemalphaSelected()
|
|||
m.loadItemsTask.searchTerm = ""
|
||||
m.VoiceBox.text = ""
|
||||
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
|
||||
m.spinner.visible = true
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -748,7 +747,6 @@ sub onvoiceFilter()
|
|||
m.loadItemsTask.NameStartsWith = " "
|
||||
m.loadItemsTask.searchTerm = m.voiceBox.text
|
||||
m.loadItemsTask.recursive = true
|
||||
m.spinner.visible = true
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -791,7 +789,7 @@ sub optionsClosed()
|
|||
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")
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
|
||||
if m.options.view <> m.view
|
||||
m.view = m.options.view
|
||||
|
@ -872,11 +870,10 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.loadItemsTask.control = "stop"
|
||||
return true
|
||||
end if
|
||||
else if key = "play" or key = "OK"
|
||||
|
||||
else if key = "play"
|
||||
itemToPlay = getItemFocused()
|
||||
|
||||
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
||||
if itemToPlay <> invalid
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
return true
|
||||
end if
|
||||
|
@ -922,7 +919,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
end if
|
||||
|
||||
if key = "replay"
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.searchTerm = ""
|
||||
m.loadItemsTask.nameStartsWith = ""
|
||||
m.voiceBox.text = ""
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="MovieLibraryView" extends="JFScreen">
|
||||
<children>
|
||||
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" />
|
||||
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation = "[25, 75]" />
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width="40" translation="[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation="[25, 75]" />
|
||||
|
||||
<maskGroup translation="[820, 0]" id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" maskSize="[1220,700]">
|
||||
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1100" height="700" opacity="1" />
|
||||
|
@ -33,21 +33,20 @@
|
|||
<Label id="selectedMovieOverview" font="font:SmallestSystemFont" translation="[120, 360]" wrap="true" lineSpacing="20" maxLines="5" width="850" ellipsisText="..." />
|
||||
|
||||
<MarkupGrid id="itemGrid" itemComponentName="GridItemSmall" numColumns="7" numRows="2" vertFocusAnimationStyle="fixed" itemSize="[230, 310]" itemSpacing="[20, 20]" />
|
||||
<RowList opacity="0" id="genrelist" translation="[120, 60]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize = "[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
|
||||
<RowList opacity="0" id="genrelist" translation="[120, 60]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize="[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
|
||||
|
||||
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
|
||||
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
|
||||
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
|
||||
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
|
||||
<ItemGridOptions id="options" visible="false" />
|
||||
<Spinner id="spinner" translation="[900, 450]" />
|
||||
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
|
||||
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
</Animation>
|
||||
<Alpha id="AlphaMenu" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="HomeLibraryItem" type="string"/>
|
||||
<field id="HomeLibraryItem" type="string" />
|
||||
<field id="parentItem" type="node" onChange="loadInitialItems" />
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||
|
@ -57,11 +56,4 @@
|
|||
<field id="showItemTitles" type="string" value="showonhover" />
|
||||
<field id="jumpToItem" type="integer" value="" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<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>
|
||||
</component>
|
|
@ -1,3 +1,6 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.itemPoster = m.top.findNode("itemPoster")
|
||||
m.postTextBackground = m.top.findNode("postTextBackground")
|
||||
|
@ -15,7 +18,7 @@ sub init()
|
|||
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
|
||||
end if
|
||||
|
||||
m.gridTitles = get_user_setting("itemgrid.gridTitles")
|
||||
m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
||||
m.posterText.visible = false
|
||||
m.postTextBackground.visible = false
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="MusicArtistGridItem" extends="Group">
|
||||
<children>
|
||||
<Poster id="backdrop" translation="[0,15]" width="280" height="280" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
|
||||
<Poster id="itemPoster" translation="[0,15]" width="280" height="280" loadDisplayMode="scaleToZoom" />
|
||||
<Rectangle id="postTextBackground" height="50" width="270" color="0x000000DD" translation = "[5, 240]">
|
||||
<Rectangle id="postTextBackground" height="50" width="270" color="0x000000DD" translation="[5, 240]">
|
||||
<ScrollingLabel id="posterText" color="#FFFFFF" maxWidth="270" height="50" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
</children>
|
||||
|
@ -11,7 +11,4 @@
|
|||
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
||||
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="MusicArtistGridItem.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,3 +1,9 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
|
||||
sub setupNodes()
|
||||
m.options = m.top.findNode("options")
|
||||
m.itemGrid = m.top.findNode("itemGrid")
|
||||
|
@ -11,7 +17,6 @@ sub setupNodes()
|
|||
m.selectedArtistGenres = m.top.findNode("selectedArtistGenres")
|
||||
m.artistLogo = m.top.findNode("artistLogo")
|
||||
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
|
||||
m.spinner = m.top.findNode("spinner")
|
||||
m.Alpha = m.top.findNode("AlphaMenu")
|
||||
m.AlphaSelected = m.top.findNode("AlphaSelected")
|
||||
m.micButton = m.top.findNode("micButton")
|
||||
|
@ -25,7 +30,7 @@ sub init()
|
|||
|
||||
m.overhang.isVisible = false
|
||||
|
||||
m.showItemCount = get_user_setting("itemgrid.showItemCount") = "true"
|
||||
m.showItemCount = m.global.session.user.settings["itemgrid.showItemCount"]
|
||||
|
||||
m.swapAnimation.observeField("state", "swapDone")
|
||||
|
||||
|
@ -71,16 +76,11 @@ sub init()
|
|||
'set inital counts for overhang before content is loaded.
|
||||
m.loadItemsTask.totalRecordCount = 0
|
||||
|
||||
m.spinner.visible = true
|
||||
|
||||
'Get reset folder setting
|
||||
m.resetGrid = get_user_setting("itemgrid.reset") = "true"
|
||||
|
||||
'Check if device has voice remote
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
|
||||
|
||||
'Hide voice search if device does not have voice remote
|
||||
if devinfo.HasFeature("voice_remote") = false
|
||||
if m.global.device.hasVoiceRemote = false
|
||||
m.micButton.visible = false
|
||||
m.micButtonText.visible = false
|
||||
end if
|
||||
|
@ -108,7 +108,7 @@ end sub
|
|||
'Load initial set of Data
|
||||
sub loadInitialItems()
|
||||
m.loadItemsTask.control = "stop"
|
||||
m.spinner.visible = true
|
||||
startLoadingSpinner(false)
|
||||
|
||||
if LCase(m.top.parentItem.json.Type) = "collectionfolder"
|
||||
m.top.HomeLibraryItem = m.top.parentItem.Id
|
||||
|
@ -120,22 +120,17 @@ sub loadInitialItems()
|
|||
SetBackground("")
|
||||
end if
|
||||
|
||||
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
|
||||
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
|
||||
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
m.sortField = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortField"]
|
||||
m.sortAscending = m.global.session.user.settings["display." + m.top.parentItem.Id + ".sortAscending"]
|
||||
m.filter = m.global.session.user.settings["display." + m.top.parentItem.Id + ".filter"]
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
|
||||
if not isValid(m.sortField) then m.sortField = "SortName"
|
||||
if not isValid(m.filter) then m.filter = "All"
|
||||
if not isValid(m.view) then m.view = "ArtistsPresentation"
|
||||
if not isValid(m.sortAscending) then m.sortAscending = true
|
||||
|
||||
if sortAscendingStr = invalid or LCase(sortAscendingStr) = "true"
|
||||
m.sortAscending = true
|
||||
else
|
||||
m.sortAscending = false
|
||||
end if
|
||||
|
||||
m.top.showItemTitles = get_user_setting("itemgrid.gridTitles")
|
||||
m.top.showItemTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
||||
|
||||
if LCase(m.top.parentItem.json.type) = "musicgenre"
|
||||
m.itemGrid.translation = "[96, 60]"
|
||||
|
@ -148,6 +143,11 @@ sub loadInitialItems()
|
|||
m.top.showItemTitles = "hidealways"
|
||||
else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
m.top.showItemTitles = "hidealways"
|
||||
else
|
||||
m.loadItemsTask.itemId = m.top.parentItem.Id
|
||||
end if
|
||||
|
@ -179,6 +179,12 @@ sub loadInitialItems()
|
|||
else if LCase(m.options.view) = "artistsgrid" or LCase(m.view) = "artistsgrid"
|
||||
m.itemGrid.translation = "[96, 60]"
|
||||
m.itemGrid.numRows = "4"
|
||||
else if LCase(m.options.view) = "albumartistsgrid" or LCase(m.view) = "albumartistsgrid"
|
||||
m.loadItemsTask.itemType = "AlbumArtists"
|
||||
m.itemGrid.translation = "[96, 60]"
|
||||
m.itemGrid.numRows = "4"
|
||||
else if LCase(m.options.view) = "albumartistspresentation" or LCase(m.view) = "albumartistspresentation"
|
||||
m.loadItemsTask.itemType = "AlbumArtists"
|
||||
else if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
|
||||
m.loadItemsTask.itemType = ""
|
||||
m.loadItemsTask.recursive = true
|
||||
|
@ -195,7 +201,6 @@ sub loadInitialItems()
|
|||
end if
|
||||
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.control = "RUN"
|
||||
SetUpOptions()
|
||||
end sub
|
||||
|
@ -206,6 +211,8 @@ sub setMusicOptions(options)
|
|||
options.views = [
|
||||
{ "Title": tr("Artists (Presentation)"), "Name": "ArtistsPresentation" },
|
||||
{ "Title": tr("Artists (Grid)"), "Name": "ArtistsGrid" },
|
||||
{ "Title": tr("Album Artists (Presentation)"), "Name": "AlbumArtistsPresentation" },
|
||||
{ "Title": tr("Album Artists (Grid)"), "Name": "AlbumArtistsGrid" },
|
||||
{ "Title": tr("Albums"), "Name": "Albums" },
|
||||
{ "Title": tr("Genres"), "Name": "Genres" }
|
||||
]
|
||||
|
@ -315,6 +322,7 @@ end sub
|
|||
'
|
||||
'Handle loaded data, and add to Grid
|
||||
sub ItemDataLoaded(msg)
|
||||
stopLoadingSpinner()
|
||||
m.top.alphaActive = false
|
||||
itemData = msg.GetData()
|
||||
m.loadItemsTask.unobserveField("content")
|
||||
|
@ -340,7 +348,6 @@ sub ItemDataLoaded(msg)
|
|||
m.loadedRows = m.loadedItems / m.genreList.numColumns
|
||||
|
||||
m.loading = false
|
||||
m.spinner.visible = false
|
||||
return
|
||||
end if
|
||||
|
||||
|
@ -363,8 +370,6 @@ sub ItemDataLoaded(msg)
|
|||
m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
|
||||
m.emptyText.visible = true
|
||||
end if
|
||||
|
||||
m.spinner.visible = false
|
||||
end sub
|
||||
|
||||
'
|
||||
|
@ -449,6 +454,10 @@ sub onItemFocused()
|
|||
return
|
||||
end if
|
||||
|
||||
if LCase(m.options.view) = "albumartistsgrid" or LCase(m.view) = "albumartistsgrid"
|
||||
return
|
||||
end if
|
||||
|
||||
if not m.selectedArtistGenres.visible
|
||||
m.selectedArtistGenres.visible = true
|
||||
end if
|
||||
|
@ -541,8 +550,9 @@ end sub
|
|||
'
|
||||
'Load next set of items
|
||||
sub loadMoreData()
|
||||
m.spinner.visible = true
|
||||
if m.Loading = true then return
|
||||
|
||||
startLoadingSpinner(false)
|
||||
m.Loading = true
|
||||
m.loadItemsTask.startIndex = m.loadedItems
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
|
@ -558,7 +568,12 @@ end sub
|
|||
'
|
||||
'Returns Focused Item
|
||||
function getItemFocused()
|
||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||
else if m.genreList.isinFocusChain() and isValid(m.genreList.itemFocused)
|
||||
return m.genreList.content.getChild(m.genreList.itemFocused)
|
||||
end if
|
||||
return invalid
|
||||
end function
|
||||
|
||||
'
|
||||
|
@ -592,7 +607,6 @@ sub onItemalphaSelected()
|
|||
m.loadItemsTask.searchTerm = ""
|
||||
m.VoiceBox.text = ""
|
||||
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
|
||||
m.spinner.visible = true
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -607,7 +621,6 @@ sub onvoiceFilter()
|
|||
m.loadItemsTask.NameStartsWith = " "
|
||||
m.loadItemsTask.searchTerm = m.voiceBox.text
|
||||
m.loadItemsTask.recursive = true
|
||||
m.spinner.visible = true
|
||||
loadInitialItems()
|
||||
end if
|
||||
end sub
|
||||
|
@ -640,7 +653,7 @@ sub optionsClosed()
|
|||
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
|
||||
end if
|
||||
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||
|
||||
if m.options.view <> m.view
|
||||
m.view = m.options.view
|
||||
|
@ -736,7 +749,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
alpha.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
|
||||
else if key = "right" and m.Alpha.isinFocusChain()
|
||||
m.top.alphaActive = false
|
||||
m.Alpha.setFocus(false)
|
||||
|
@ -746,14 +758,12 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.genreList.setFocus(m.genreList.opacity = 1)
|
||||
|
||||
return true
|
||||
|
||||
else if key = "replay" and m.itemGrid.isinFocusChain()
|
||||
if m.resetGrid = true
|
||||
m.itemGrid.animateToItem = 0
|
||||
else
|
||||
m.itemGrid.jumpToItem = 0
|
||||
end if
|
||||
|
||||
else if key = "replay" and m.genreList.isinFocusChain()
|
||||
if m.resetGrid = true
|
||||
m.genreList.animateToItem = 0
|
||||
|
@ -761,10 +771,15 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.genreList.jumpToItem = 0
|
||||
end if
|
||||
return true
|
||||
else if key = "play"
|
||||
itemToPlay = getItemFocused()
|
||||
if itemToPlay <> invalid
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
if key = "replay"
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.searchTerm = ""
|
||||
m.loadItemsTask.nameStartsWith = ""
|
||||
m.voiceBox.text = ""
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="MusicLibraryView" extends="JFScreen">
|
||||
<children>
|
||||
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" />
|
||||
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation = "[25, 75]" />
|
||||
<VoiceTextEditBox id="VoiceBox" visible="true" width="40" translation="[52, 120]" />
|
||||
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation="[25, 75]" />
|
||||
|
||||
<maskGroup translation="[820, 0]" id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" maskSize="[1220,445]">
|
||||
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1100" height="450" opacity="1" />
|
||||
|
@ -21,18 +21,17 @@
|
|||
<MarkupGrid id="genrelist" itemComponentName="MusicArtistGridItem" numColumns="6" numRows="4" vertFocusAnimationStyle="fixed" translation="[96, 60]" itemSize="[280, 280]" itemSpacing="[20, 20]" opacity="0" />
|
||||
|
||||
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
|
||||
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
|
||||
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
|
||||
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
|
||||
<ItemGridOptions id="options" visible="false" />
|
||||
<Spinner id="spinner" translation="[900, 450]" />
|
||||
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
|
||||
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
|
||||
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
|
||||
</Animation>
|
||||
<Alpha id="AlphaMenu" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="HomeLibraryItem" type="string"/>
|
||||
<field id="HomeLibraryItem" type="string" />
|
||||
<field id="parentItem" type="node" onChange="loadInitialItems" />
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||
|
@ -42,10 +41,4 @@
|
|||
<field id="showItemTitles" type="string" value="showonhover" />
|
||||
<field id="jumpToItem" type="integer" value="" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<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="MusicLibraryView.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFButton" extends="Button">
|
||||
<interface>
|
||||
<field id="minChars" type="int" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFButton.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFGroup" extends="Group">
|
||||
<interface>
|
||||
<field id="backPressed" type="boolean" alwaysNotify="true" />
|
||||
|
@ -7,5 +7,4 @@
|
|||
<field id="optionsAvailable" value="true" type="boolean" />
|
||||
<field id="overhangVisible" value="true" type="boolean" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFGroup.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFMessageDialog" extends="JFGroup">
|
||||
<children>
|
||||
<Poster
|
||||
id="dialogBackground"
|
||||
uri="pkg:/images/dialog.9.png"
|
||||
blendColor="#000000"
|
||||
translation="[0, 0]"
|
||||
/>
|
||||
id="dialogBackground"
|
||||
uri="pkg:/images/dialog.9.png"
|
||||
blendColor="#000000"
|
||||
translation="[0, 0]"
|
||||
/>
|
||||
<Label id="messageText" horizAlign="center" wrap="true" />
|
||||
<LabelList id="optionList" vertFocusAnimationStyle="floatingFocus" textHorizAlign="center">
|
||||
<ContentNode id = "content" role = "content"></ContentNode>
|
||||
<ContentNode id="content" role="content"></ContentNode>
|
||||
</LabelList>
|
||||
|
||||
|
||||
</children>
|
||||
<interface>
|
||||
<field id="id" type="string" />
|
||||
|
@ -20,5 +20,4 @@
|
|||
<field id="fontHeight" type="integer" />
|
||||
<field id="fontWidth" type="integer" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFMessageDialog.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,3 +1,5 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.top.id = "overhang"
|
||||
' hide seperators till they're needed
|
||||
|
@ -16,11 +18,8 @@ sub init()
|
|||
m.slideDownAnimation = m.top.findNode("slideDown")
|
||||
m.slideUpAnimation = m.top.findNode("slideUp")
|
||||
' show clock based on user setting
|
||||
m.hideClock = get_user_setting("ui.design.hideclock") = "true"
|
||||
m.hideClock = m.global.session.user.settings["ui.design.hideclock"]
|
||||
if not m.hideClock
|
||||
' get system preference clock format (12/24hr)
|
||||
di = CreateObject("roDeviceInfo")
|
||||
m.clockFormat = di.GetClockFormat()
|
||||
' save node references
|
||||
m.overlayHours = m.top.findNode("overlayHours")
|
||||
m.overlayMinutes = m.top.findNode("overlayMinutes")
|
||||
|
@ -105,7 +104,7 @@ sub resetTime()
|
|||
end sub
|
||||
|
||||
sub updateTimeDisplay()
|
||||
if m.clockFormat = "24h"
|
||||
if m.global.device.clockFormat = "24h"
|
||||
m.overlayMeridian.text = ""
|
||||
if m.currentHours < 10
|
||||
m.overlayHours.text = "0" + StrI(m.currentHours).trim()
|
|
@ -1,14 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFOverhang" extends="Group">
|
||||
<children>
|
||||
<Poster id="overlayLogo" uri="pkg:/images/logo.png" translation="[70, 53]" width="270" height="72" />
|
||||
<LayoutGroup id="overlayLeftGroup" layoutDirection="horiz" translation="[375, 53]" itemSpacings="30">
|
||||
<Rectangle id="overlayLeftSeperator" color="#666666" width="2" height="64"/>
|
||||
<Rectangle id="overlayLeftSeperator" color="#666666" width="2" height="64" />
|
||||
<ScrollingLabel id="overlayTitle" font="font:LargeSystemFont" vertAlign="center" height="64" maxWidth="1100" repeatCount="0" />
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="overlayRightGroup" layoutDirection="horiz" itemSpacings="30" translation="[1820, 53]" horizAlignment="right">
|
||||
<Label id="overlayCurrentUser" font="font:MediumSystemFont" width="300" horizAlign="right" vertAlign="center" height="64" />
|
||||
<Rectangle id="overlayRightSeperator" color="#666666" width="2" height="64" visible="false"/>
|
||||
<Rectangle id="overlayRightSeperator" color="#666666" width="2" height="64" visible="false" />
|
||||
<LayoutGroup id="overlayTimeGroup" layoutDirection="horiz" horizAlignment="right" itemSpacings="0">
|
||||
<Label id="overlayHours" font="font:MediumSystemFont" vertAlign="center" height="64" />
|
||||
<Label font="font:MediumSystemFont" text=":" vertAlign="center" height="64" />
|
||||
|
@ -40,6 +40,4 @@
|
|||
<field id="disableMoveAnimation" value="false" type="boolean" />
|
||||
<function name="resetTime" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFOverhang.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,6 +1,29 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
m.top.backgroundColor = "#262626" '"#101010"
|
||||
m.top.backgroundURI = ""
|
||||
m.spinner = m.top.findNode("spinner")
|
||||
end sub
|
||||
|
||||
' Triggered when the isLoading boolean component field is changed
|
||||
sub isLoadingChanged()
|
||||
m.spinner.visible = m.top.isLoading
|
||||
end sub
|
||||
|
||||
' Triggered when the disableRemote boolean component field is changed
|
||||
sub disableRemoteChanged()
|
||||
if m.top.disableRemote
|
||||
dialog = createObject("roSGNode", "ProgressDialog")
|
||||
dialog.id = "invisibiledialog"
|
||||
dialog.visible = false
|
||||
dialog.opacity = 0
|
||||
m.top.dialog = dialog
|
||||
else
|
||||
if isValid(m.top.dialog)
|
||||
m.top.dialog.close = true
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
|
@ -1,12 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFScene" extends="Scene">
|
||||
<children>
|
||||
<Group id="content" />
|
||||
<JFOverhang id="overhang" />
|
||||
<Spinner id="spinner" translation="[897, 477]" visible="false" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="disableRemote" type="boolean" value="false" onchange="disableRemoteChanged" />
|
||||
<field id="isLoading" type="boolean" value="false" onchange="isLoadingChanged" />
|
||||
<field id="exit" type="boolean" alwaysNotify="true" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFScene.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,3 +1,10 @@
|
|||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||
|
||||
sub init()
|
||||
' initialize the log manager. second param sets log output:
|
||||
' 1 error, 2 warn, 3 info, 4 verbose, 5 debug
|
||||
_rLog = log_initializeLogManager(["log_PrintTransport"], 5) 'bs:disable-line
|
||||
end sub
|
||||
' Function called when the screen is displayed by the screen manager
|
||||
' It is expected that screens override this function to handle focus
|
||||
' managmenet and any other actions required on screen shown
|
||||
|
@ -14,3 +21,4 @@ end sub
|
|||
' to handle focus any actions required on the screen being hidden
|
||||
sub OnScreenHidden()
|
||||
end sub
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFScreen" extends="JFGroup">
|
||||
<interface>
|
||||
<function name="OnScreenShown" />
|
||||
<function name="OnScreenHidden" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFScreen.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,3 +1,6 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.playbackTimer = m.top.findNode("playbackTimer")
|
||||
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
|
||||
|
@ -10,7 +13,7 @@ sub init()
|
|||
m.top.transcodeReasons = []
|
||||
m.bufferCheckTimer.duration = 30
|
||||
|
||||
if get_user_setting("ui.design.hideclock") = "true"
|
||||
if m.global.session.user.settings["ui.design.hideclock"] = true
|
||||
clockNode = findNodeBySubtype(m.top, "clock")
|
||||
if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node)
|
||||
end if
|
||||
|
@ -19,12 +22,7 @@ sub init()
|
|||
m.nextEpisodeButton = m.top.findNode("nextEpisode")
|
||||
m.nextEpisodeButton.text = tr("Next Episode")
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
m.nextupbuttonseconds = get_user_setting("playback.nextupbuttonseconds", "30")
|
||||
if isValid(m.nextupbuttonseconds)
|
||||
m.nextupbuttonseconds = val(m.nextupbuttonseconds)
|
||||
else
|
||||
m.nextupbuttonseconds = 30
|
||||
end if
|
||||
m.nextupbuttonseconds = m.global.session.user.settings["playback.nextupbuttonseconds"].ToInt()
|
||||
|
||||
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
|
||||
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
|
||||
|
@ -46,7 +44,7 @@ sub onAllowCaptionsChange()
|
|||
m.captionTask.observeField("useThis", "checkCaptionMode")
|
||||
m.top.observeField("currentSubtitleTrack", "loadCaption")
|
||||
m.top.observeField("globalCaptionMode", "toggleCaption")
|
||||
if get_user_setting("playback.subs.custom") = "false"
|
||||
if m.global.session.user.settings["playback.subs.custom"] = false
|
||||
m.top.suppressCaptions = false
|
||||
else
|
||||
m.top.suppressCaptions = true
|
||||
|
@ -92,10 +90,13 @@ end sub
|
|||
'
|
||||
' Runs Next Episode button animation and sets focus to button
|
||||
sub showNextEpisodeButton()
|
||||
if m.global.userConfig.EnableNextEpisodeAutoPlay and not m.nextEpisodeButton.visible
|
||||
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||
|
||||
if m.nextEpisodeButton.opacity = 0 and m.global.session.user.configuration.EnableNextEpisodeAutoPlay
|
||||
m.nextEpisodeButton.visible = true
|
||||
m.showNextEpisodeButtonAnimation.control = "start"
|
||||
m.nextEpisodeButton.setFocus(true)
|
||||
m.nextEpisodeButton.visible = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -119,13 +120,22 @@ end sub
|
|||
|
||||
' Checks if we need to display the Next Episode button
|
||||
sub checkTimeToDisplayNextEpisode()
|
||||
if m.top.content.contenttype <> 4 then return
|
||||
if m.nextupbuttonseconds = 0 then return
|
||||
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||
|
||||
if int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds)
|
||||
showNextEpisodeButton()
|
||||
updateCount()
|
||||
return
|
||||
if isValid(m.top.duration) and isValid(m.top.position)
|
||||
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
|
||||
|
||||
if nextEpisodeCountdown < 0 and m.nextEpisodeButton.opacity = 0.9
|
||||
hideNextEpisodeButton()
|
||||
return
|
||||
else if nextEpisodeCountdown > 1 and int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds - 1)
|
||||
updateCount()
|
||||
if m.nextEpisodeButton.opacity = 0
|
||||
showNextEpisodeButton()
|
||||
end if
|
||||
return
|
||||
end if
|
||||
end if
|
||||
|
||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||
|
@ -268,8 +278,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
return true
|
||||
else
|
||||
'Hide Next Episode Button
|
||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||
m.nextEpisodeButton.visible = false
|
||||
if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus()
|
||||
m.nextEpisodeButton.opacity = 0
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
m.top.setFocus(true)
|
||||
end if
|
||||
|
@ -284,9 +294,13 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.top.selectPlaybackInfoPressed = true
|
||||
return true
|
||||
else if key = "OK"
|
||||
' OK will play/pause depending on current state
|
||||
' return false to allow selection during seeking
|
||||
if m.top.state = "paused"
|
||||
if m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
|
||||
m.top.state = "finished"
|
||||
hideNextEpisodeButton()
|
||||
return true
|
||||
else if m.top.state = "paused"
|
||||
' OK will play/pause depending on current state
|
||||
' return false to allow selection during seeking
|
||||
m.top.control = "resume"
|
||||
return false
|
||||
else if m.top.state = "playing"
|
|
@ -23,11 +23,6 @@
|
|||
<field id="mediaSourceId" type="string" />
|
||||
<field id="audioIndex" type="integer" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFVideo.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
|
||||
<children>
|
||||
<Group id="captionGroup" translation="[960,1020]"></Group>
|
||||
|
||||
|
@ -45,7 +40,7 @@
|
|||
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
||||
</Animation>
|
||||
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
|
||||
<Animation id="hideNextEpisodeButton" duration=".25" repeat="false" easeFunction="inQuad">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
||||
</Animation>
|
||||
</children>
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
m.title = m.top.findNode("title")
|
||||
m.staticTitle = m.top.findNode("staticTitle")
|
||||
|
@ -8,8 +11,6 @@ sub init()
|
|||
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
|
||||
m.deviceInfo = CreateObject("roDeviceInfo")
|
||||
|
||||
' Randmomise the background colors
|
||||
posterBackgrounds = m.global.constants.poster_bg_pallet
|
||||
m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
|
@ -55,7 +56,7 @@ sub itemContentChanged() as void
|
|||
itemData = m.top.itemContent
|
||||
m.title.text = itemData.title
|
||||
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
|
@ -79,7 +80,7 @@ sub itemContentChanged() as void
|
|||
|
||||
imageUrl = itemData.posterURL
|
||||
|
||||
if get_user_setting("ui.tvshows.blurunwatched") = "true"
|
||||
if m.global.session.user.settings["ui.tvshows.blurunwatched"] = true
|
||||
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.userdata)
|
||||
if not itemData.json.userdata.played
|
||||
imageUrl = imageUrl + "&blur=15"
|
||||
|
@ -101,7 +102,7 @@ sub focusChanged()
|
|||
m.staticTitle.visible = false
|
||||
m.title.visible = true
|
||||
' text to speech for accessibility
|
||||
if m.deviceInfo.IsAudioGuideEnabled() = true
|
||||
if m.global.device.isAudioGuideEnabled = true
|
||||
txt2Speech = CreateObject("roTextToSpeech")
|
||||
txt2Speech.Flush()
|
||||
txt2Speech.Say(m.title.text)
|
|
@ -5,7 +5,7 @@
|
|||
<ScrollingLabel id="Series" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
|
||||
<Poster id="poster" translation="[2,0]" loadDisplayMode="scaleToFit">
|
||||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[104, 0]">
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
</Poster>
|
||||
<ScrollingLabel id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
|
||||
|
@ -16,7 +16,4 @@
|
|||
<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" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
17
components/OverviewDialog.bs
Normal file
17
components/OverviewDialog.bs
Normal file
|
@ -0,0 +1,17 @@
|
|||
sub setTitle()
|
||||
m.top.findNode("titleArea").primaryTitle = m.top.title
|
||||
end sub
|
||||
|
||||
sub setOverview()
|
||||
m.top.findNode("description").text = m.top.overview
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if press = false then return false
|
||||
|
||||
if key = "OK" and m.top.findNode("contentArea").isInFocusChain()
|
||||
m.top.close = true
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
|
@ -1,35 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="OverviewDialog" extends="StandardDialog">
|
||||
<interface>
|
||||
<field id="Title" type="string" onchange="setTitle" />
|
||||
<field id="Overview" type="string" onChange="setOverview" />
|
||||
</interface>
|
||||
<script type="text/brightscript">
|
||||
<![CDATA[
|
||||
sub setTitle()
|
||||
m.top.findNode("titleArea").primaryTitle = m.top.title
|
||||
end sub
|
||||
|
||||
sub setOverview()
|
||||
m.top.findNode("description").text = m.top.overview
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if press = false then return false
|
||||
|
||||
if key = "OK" and m.top.findNode("contentArea").isInFocusChain()
|
||||
m.top.close = true
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
||||
]]>
|
||||
</script>
|
||||
<children>
|
||||
<StdDlgTitleArea id="titleArea" />
|
||||
<StdDlgContentArea id="contentArea">
|
||||
<StdDlgTextItem id="description"
|
||||
namedTextStyle="normal" />
|
||||
</StdDlgContentArea>
|
||||
</children>
|
||||
<interface>
|
||||
<field id="Title" type="string" onchange="setTitle" />
|
||||
<field id="Overview" type="string" onChange="setOverview" />
|
||||
</interface>
|
||||
<children>
|
||||
<StdDlgTitleArea id="titleArea" />
|
||||
<StdDlgContentArea id="contentArea">
|
||||
<StdDlgTextItem id="description"
|
||||
namedTextStyle="normal" />
|
||||
</StdDlgContentArea>
|
||||
</children>
|
||||
</component>
|
|
@ -1,3 +1,7 @@
|
|||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.dscr = m.top.findNode("description")
|
||||
m.vidsList = m.top.findNode("extrasGrid")
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="PersonDetails" extends="JFGroup">
|
||||
<interface>
|
||||
<field id="itemContent" type="node" onChange="loadPerson" />
|
||||
|
@ -7,26 +7,26 @@
|
|||
</interface>
|
||||
<children>
|
||||
<LayoutGroup id="main_group"
|
||||
layoutdirection="vert" translation="[60, 180]" itemSpacings="[36]" >
|
||||
<LayoutGroup id="header_group" layoutdirection="horiz" >
|
||||
<LayoutGroup id="title_group" layoutdirection="vert" itemSpacings="[11]" >
|
||||
<Rectangle id="title_rectangle" height="100" width="1426" color="#262626">
|
||||
<Label id="name" font="font:LargeBoldSystemFont" height="100" width="1426" vertAlign="bottom" />
|
||||
</Rectangle>
|
||||
</LayoutGroup>
|
||||
<ButtonGroupHoriz id="buttons" >
|
||||
<Button id="favorite-button" text="Favorite" iconUri="" focusedIconUri="" />
|
||||
</ButtonGroupHoriz>
|
||||
layoutdirection="vert" translation="[60, 180]" itemSpacings="[36]">
|
||||
<LayoutGroup id="header_group" layoutdirection="horiz">
|
||||
<LayoutGroup id="title_group" layoutdirection="vert" itemSpacings="[11]">
|
||||
<Rectangle id="title_rectangle" height="100" width="1426" color="#262626">
|
||||
<Label id="name" font="font:LargeBoldSystemFont" height="100" width="1426" vertAlign="bottom" />
|
||||
</Rectangle>
|
||||
</LayoutGroup>
|
||||
<ButtonGroupHoriz id="buttons">
|
||||
<Button id="favorite-button" text="Favorite" iconUri="" focusedIconUri="" />
|
||||
</ButtonGroupHoriz>
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="personInfoGroup"
|
||||
layoutDirection="horiz" itemSpacings="[46]">
|
||||
layoutDirection="horiz" itemSpacings="[46]">
|
||||
<Poster id="personImage"
|
||||
width="430" height="645" />
|
||||
width="430" height="645" />
|
||||
<LayoutGroup id="vertSpacer" layoutDirection="vert" itemSpacings="[24]">
|
||||
<LayoutGroup id="dataGroup>" layoutDirection="vert" translation="[450,180]">
|
||||
<Rectangle id="dscrBorder" height="645" width="1322" color="0x202020ff" visible="true">
|
||||
<Rectangle id='dscrRect' translation="[3, 3]" height="639" width="1316" color="0x202020ff">
|
||||
<Label id="description"
|
||||
<Label id="description"
|
||||
height="627" width="1280" wrap="true" translation="[18, 15]"
|
||||
font="font:SmallestSystemFont" color="#e4e4e4ff" ellipsisText=" ... (-OK- for More)" />
|
||||
</Rectangle>
|
||||
|
@ -37,8 +37,4 @@
|
|||
</LayoutGroup>
|
||||
<extrasSlider id="personVideos" />
|
||||
</children>
|
||||
<script type="text/brightscript" uri="PersonDetails.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- A PlaybackDialog is a regular dialog, except it takes key releases -->
|
||||
<!-- instead of presses so that key releases don't fall into other listeners -->
|
||||
<component name="PlaybackDialog" extends="Dialog">
|
||||
<script type="text/brightscript" uri="PlaybackDialog.brs" />
|
||||
|
||||
</component>
|
4
components/PlayedCheckmark.bs
Normal file
4
components/PlayedCheckmark.bs
Normal file
|
@ -0,0 +1,4 @@
|
|||
sub init()
|
||||
checkmark = m.top.findNode("checkmark")
|
||||
checkmark.font.size = 48
|
||||
end sub
|
6
components/PlayedCheckmark.xml
Normal file
6
components/PlayedCheckmark.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="PlayedCheckmark" extends="Rectangle">
|
||||
<children>
|
||||
<Label id="checkmark" width="60" height="50" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="bottom" text="✓" />
|
||||
</children>
|
||||
</component>
|
105
components/PlaystateTask.brs → components/PlaystateTask.bs
Executable file → Normal file
105
components/PlaystateTask.brs → components/PlaystateTask.bs
Executable file → Normal file
|
@ -1,51 +1,54 @@
|
|||
sub init()
|
||||
m.top.functionName = "PlaystateUpdate"
|
||||
end sub
|
||||
|
||||
sub PlaystateUpdate()
|
||||
if m.top.status = "start"
|
||||
url = "Sessions/Playing"
|
||||
else if m.top.status = "stop"
|
||||
url = "Sessions/Playing/Stopped"
|
||||
else if m.top.status = "update"
|
||||
url = "Sessions/Playing/Progress"
|
||||
else
|
||||
' Unknown State
|
||||
return
|
||||
end if
|
||||
params = PlaystateDefaults(m.top.params)
|
||||
resp = APIRequest(url)
|
||||
postJson(resp, params)
|
||||
end sub
|
||||
|
||||
function PlaystateDefaults(params = {} as object)
|
||||
new_params = {
|
||||
'"CanSeek": false
|
||||
'"Item": "{}", ' TODO!
|
||||
'"NowPlayingQueue": "[]", ' TODO!
|
||||
'"PlaylistItemId": "",
|
||||
'"ItemId": id,
|
||||
'"SessionId": "", ' TODO!
|
||||
'"MediaSourceId": id,
|
||||
'"AudioStreamIndex": 1,
|
||||
'"SubtitleStreamIndex": 0,
|
||||
"IsPaused": false,
|
||||
'"IsMuted": false,
|
||||
"PositionTicks": 0
|
||||
'"PlaybackStartTimeTicks": 0,
|
||||
'"VolumeLevel": 100,
|
||||
'"Brightness": 100,
|
||||
'"AspectRatio": "16x9",
|
||||
'"PlayMethod": "DirectStream"
|
||||
'"LiveStreamId": "",
|
||||
'"PlaySessionId": "",
|
||||
'"RepeatMode": "RepeatNone"
|
||||
}
|
||||
|
||||
paramsArray = params.items()
|
||||
for i = 0 to paramsArray.count() - 1
|
||||
item = paramsArray[i]
|
||||
new_params[item.key] = item.value
|
||||
end for
|
||||
return FormatJson(new_params)
|
||||
end function
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
|
||||
sub init()
|
||||
m.top.functionName = "PlaystateUpdate"
|
||||
end sub
|
||||
|
||||
sub PlaystateUpdate()
|
||||
if m.top.status = "start"
|
||||
url = "Sessions/Playing"
|
||||
else if m.top.status = "stop"
|
||||
url = "Sessions/Playing/Stopped"
|
||||
else if m.top.status = "update"
|
||||
url = "Sessions/Playing/Progress"
|
||||
else
|
||||
' Unknown State
|
||||
return
|
||||
end if
|
||||
params = PlaystateDefaults(m.top.params)
|
||||
resp = APIRequest(url)
|
||||
postJson(resp, params)
|
||||
end sub
|
||||
|
||||
function PlaystateDefaults(params = {} as object)
|
||||
new_params = {
|
||||
'"CanSeek": false
|
||||
'"Item": "{}", ' TODO!
|
||||
'"NowPlayingQueue": "[]", ' TODO!
|
||||
'"PlaylistItemId": "",
|
||||
'"ItemId": id,
|
||||
'"SessionId": "", ' TODO!
|
||||
'"MediaSourceId": id,
|
||||
'"AudioStreamIndex": 1,
|
||||
'"SubtitleStreamIndex": 0,
|
||||
"IsPaused": false,
|
||||
'"IsMuted": false,
|
||||
"PositionTicks": 0
|
||||
'"PlaybackStartTimeTicks": 0,
|
||||
'"VolumeLevel": 100,
|
||||
'"Brightness": 100,
|
||||
'"AspectRatio": "16x9",
|
||||
'"PlayMethod": "DirectStream"
|
||||
'"LiveStreamId": "",
|
||||
'"PlaySessionId": "",
|
||||
'"RepeatMode": "RepeatNone"
|
||||
}
|
||||
|
||||
paramsArray = params.items()
|
||||
for i = 0 to paramsArray.count() - 1
|
||||
item = paramsArray[i]
|
||||
new_params[item.key] = item.value
|
||||
end for
|
||||
return FormatJson(new_params)
|
||||
end function
|
|
@ -1,11 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<component name="PlaystateTask" extends="Task">
|
||||
<interface>
|
||||
<field id="status" type="string" />
|
||||
<field id="params" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="PlaystateTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
|
@ -1,33 +0,0 @@
|
|||
sub init()
|
||||
m.content = m.top.findNode("content")
|
||||
m.top.observeField("contentData", "onContentDataChanged")
|
||||
|
||||
m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
|
||||
|
||||
m.top.id = "OKDialog"
|
||||
m.top.height = 900
|
||||
m.top.title = "What's New?"
|
||||
m.top.buttons = [tr("OK")]
|
||||
end sub
|
||||
|
||||
sub onButtonSelected()
|
||||
if m.top.buttonSelected = 0
|
||||
m.global.sceneManager.returnData = m.top.contentData.data[m.content.selectedIndex]
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onContentDataChanged()
|
||||
i = 0
|
||||
for each item in m.top.contentData.data
|
||||
cardItem = m.content.CreateChild("StdDlgActionCardItem")
|
||||
cardItem.iconType = "radiobutton"
|
||||
|
||||
if isValid(item.selected)
|
||||
m.content.selectedIndex = i
|
||||
end if
|
||||
|
||||
textLine = cardItem.CreateChild("SimpleLabel")
|
||||
textLine.text = item.description
|
||||
i++
|
||||
end for
|
||||
end sub
|
138
components/RadioDialog.bs
Normal file
138
components/RadioDialog.bs
Normal file
|
@ -0,0 +1,138 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
m.contentArea = m.top.findNode("contentArea")
|
||||
m.radioOptions = m.top.findNode("radioOptions")
|
||||
m.scrollBarColumn = []
|
||||
|
||||
m.top.observeField("contentData", "onContentDataChanged")
|
||||
m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
|
||||
|
||||
m.radioOptions.observeField("focusedChild", "onItemFocused")
|
||||
|
||||
m.top.id = "OKDialog"
|
||||
m.top.height = 900
|
||||
end sub
|
||||
|
||||
' Event handler for when user selected a button
|
||||
sub onButtonSelected()
|
||||
if m.top.buttonSelected = 0
|
||||
m.global.sceneManager.returnData = m.top.contentData.data[m.radioOptions.selectedIndex]
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Event handler for when user's cursor highlights an option in the option list
|
||||
sub onItemFocused()
|
||||
focusedChild = m.radioOptions.focusedChild
|
||||
if not isValid(focusedChild) then return
|
||||
|
||||
moveScrollBar()
|
||||
|
||||
' If the option list is scrollable, move the option list to the user's section
|
||||
if m.scrollBarColumn.count() <> 0
|
||||
hightedButtonTranslation = m.radioOptions.focusedChild.translation
|
||||
m.radioOptions.translation = [m.radioOptions.translation[0], -1 * hightedButtonTranslation[1]]
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
||||
' Move the popup's scroll bar
|
||||
sub moveScrollBar()
|
||||
' If we haven't found the scrollbar column node yet, try to find it now
|
||||
if m.scrollBarColumn.count() = 0
|
||||
scrollBar = findNodeBySubtype(m.contentArea, "StdDlgScrollbar")
|
||||
if scrollBar.count() = 0 or not isValid(scrollBar[0]) or not isValid(scrollBar[0].node)
|
||||
return
|
||||
end if
|
||||
|
||||
m.scrollBarColumn = findNodeBySubtype(scrollBar[0].node, "Poster")
|
||||
if m.scrollBarColumn.count() = 0 or not isValid(m.scrollBarColumn[0]) or not isValid(m.scrollBarColumn[0].node)
|
||||
return
|
||||
end if
|
||||
|
||||
m.scrollBarThumb = findNodeBySubtype(m.scrollBarColumn[0].node, "Poster")
|
||||
if m.scrollBarThumb.count() = 0 or not isValid(m.scrollBarThumb[0]) or not isValid(m.scrollBarThumb[0].node)
|
||||
return
|
||||
end if
|
||||
|
||||
m.scrollBarThumb[0].node.blendColor = "#444444"
|
||||
' If the user presses left then right, it's possible for us to lose focus. Ensure focus stays on the option list.
|
||||
scrollBar[0].node.observeField("focusedChild", "onScrollBarFocus")
|
||||
|
||||
' Hide the default scrollbar background
|
||||
m.scrollBarColumn[0].node.uri = ""
|
||||
|
||||
' Create a new scrollbar background so we can move the original nodes freely
|
||||
scrollbarBackground = createObject("roSGNode", "Rectangle")
|
||||
scrollbarBackground.color = "#101010"
|
||||
scrollbarBackground.opacity = "0.3"
|
||||
scrollbarBackground.width = "30"
|
||||
scrollbarBackground.height = m.contentArea.clippingRect.height
|
||||
scrollbarBackground.translation = [0, 0]
|
||||
scrollBar[0].node.insertChild(scrollbarBackground, 0)
|
||||
|
||||
' Determine the proper scroll amount for the scrollbar
|
||||
m.scrollAmount = (m.contentArea.clippingRect.height - int(m.scrollBarThumb[0].node.height)) / m.radioOptions.getChildCount()
|
||||
m.scrollAmount += m.scrollAmount / m.radioOptions.getChildCount()
|
||||
end if
|
||||
|
||||
if not isvalid(m.radioOptions.focusedChild.id) then return
|
||||
|
||||
m.scrollBarColumn[0].node.translation = [0, val(m.radioOptions.focusedChild.id) * m.scrollAmount]
|
||||
end sub
|
||||
|
||||
' If somehow the scrollbar gains focus, set focus back to the option list
|
||||
sub onScrollBarFocus()
|
||||
m.radioOptions.setFocus(true)
|
||||
|
||||
' Ensure scrollbar styles remain in an unfocused state
|
||||
m.scrollBarThumb[0].node.blendColor = "#353535"
|
||||
end sub
|
||||
|
||||
' Once user selected an item, move cursor down to OK button
|
||||
sub onItemSelected()
|
||||
buttonArea = findNodeBySubtype(m.top, "StdDlgButtonArea")
|
||||
|
||||
if buttonArea.count() <> 0 and isValid(buttonArea[0]) and isValid(buttonArea[0].node)
|
||||
buttonArea[0].node.setFocus(true)
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onContentDataChanged()
|
||||
i = 0
|
||||
for each item in m.top.contentData.data
|
||||
cardItem = m.radioOptions.CreateChild("StdDlgActionCardItem")
|
||||
cardItem.iconType = "radiobutton"
|
||||
cardItem.id = i
|
||||
|
||||
if isValid(item.selected)
|
||||
m.radioOptions.selectedIndex = i
|
||||
end if
|
||||
|
||||
textLine = cardItem.CreateChild("SimpleLabel")
|
||||
textLine.text = item.track.description
|
||||
cardItem.observeField("selected", "onItemSelected")
|
||||
i++
|
||||
end for
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if key = "right"
|
||||
' By default RIGHT from the option list selects the OK button
|
||||
' Instead, keep the user on the option list
|
||||
return true
|
||||
end if
|
||||
|
||||
if not press then return false
|
||||
|
||||
if key = "up"
|
||||
' By default UP from the OK button is the scrollbar
|
||||
' Instead, move the user to the option list
|
||||
if not m.radioOptions.isinFocusChain()
|
||||
m.radioOptions.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
|
@ -1,13 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="RadioDialog" extends="StandardMessageDialog">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="RadioDialog" extends="StandardMessageDialog" initialFocus="radioOptions">
|
||||
<children>
|
||||
<StdDlgContentArea>
|
||||
<StdDlgItemGroup id="content" />
|
||||
<StdDlgContentArea id="contentArea">
|
||||
<StdDlgItemGroup id="radioOptions" />
|
||||
</StdDlgContentArea>
|
||||
</children>
|
||||
<interface>
|
||||
<field id="contentData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="RadioDialog.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
|
@ -1,3 +1,9 @@
|
|||
import "pkg:/source/api/Items.bs"
|
||||
import "pkg:/source/api/baserequest.bs"
|
||||
import "pkg:/source/utils/config.bs"
|
||||
import "pkg:/source/api/Image.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
|
||||
sub init()
|
||||
m.top.layoutDirection = "vert"
|
||||
m.top.horizAlignment = "center"
|
|
@ -1,16 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="SearchBox" extends="LayoutGroup">
|
||||
<children>
|
||||
<Label id = "text" text="" visible="false" />
|
||||
<DynamicMiniKeyboard id="search_Key" />
|
||||
<Label id="text" text="" visible="false" />
|
||||
<DynamicMiniKeyboard id="search_Key" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="search_values" type="string" alwaysNotify="true" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="SearchBox.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
</component>
|
||||
</component>
|
|
@ -2,5 +2,5 @@ sub init()
|
|||
m.top.poster.uri = "pkg:/images/spinner.png"
|
||||
m.top.control = "start"
|
||||
m.top.clockwise = true
|
||||
m.top.spinInterval = 3
|
||||
m.top.spinInterval = 1
|
||||
end sub
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="Spinner" extends="BusySpinner">
|
||||
<script type="text/brightscript" uri="Spinner.brs" />
|
||||
</component>
|
||||
|
||||
</component>
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="StandardDialog" extends="StandardMessageDialog">
|
||||
<children>
|
||||
<StdDlgContentArea id="content" />
|
||||
|
@ -6,5 +6,4 @@
|
|||
<interface>
|
||||
<field id="contentData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="StandardDialog.brs" />
|
||||
</component>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user