diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..8b344eb90 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @jellyfin/roku \ No newline at end of file diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..23cc23ea7 --- /dev/null +++ b/.github/release.yml @@ -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 diff --git a/.github/workflows/auto-close-stale-pr.yml b/.github/workflows/auto-close-stale-pr.yml index 6ae3fec5a..360f18ac2 100644 --- a/.github/workflows/auto-close-stale-pr.yml +++ b/.github/workflows/auto-close-stale-pr.yml @@ -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 }} diff --git a/.github/workflows/automations.yml b/.github/workflows/automations.yml index d7a2f3df6..20210f281 100644 --- a/.github/workflows/automations.yml +++ b/.github/workflows/automations.yml @@ -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 }} diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index fd5e902ec..759e7bd07 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -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 \ No newline at end of file + path: ${{ github.workspace }}/build/staging + if-no-files-found: error diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 000000000..b380a1519 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -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 diff --git a/.github/workflows/build-prod.yml b/.github/workflows/build-prod.yml index bb3039d92..8bf6ac557 100644 --- a/.github/workflows/build-prod.yml +++ b/.github/workflows/build-prod.yml @@ -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 \ No newline at end of file + name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }} + path: ${{ github.workspace }}/build/staging + if-no-files-found: error diff --git a/.github/workflows/deploy-api-docs.yml b/.github/workflows/deploy-api-docs.yml new file mode 100644 index 000000000..b4a6787f1 --- /dev/null +++ b/.github/workflows/deploy-api-docs.yml @@ -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 diff --git a/.github/workflows/roku-analysis.yml b/.github/workflows/roku-analysis.yml new file mode 100644 index 000000000..d83efb9f1 --- /dev/null +++ b/.github/workflows/roku-analysis.yml @@ -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 diff --git a/.gitignore b/.gitignore index 3d5bcad8f..4ce5573ca 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.vscode/brighterscript.code-snippets b/.vscode/brighterscript.code-snippets new file mode 100644 index 000000000..23b25e21e --- /dev/null +++ b/.vscode/brighterscript.code-snippets @@ -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" + ] + } +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d09f15bf9..3d5cc1b1c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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": [] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 4a9b32001..fe6b11d6c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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 } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 65c7ba476..746c772b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..039935d0b --- /dev/null +++ b/.vscode/tasks.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index e42dd3c56..fdef679b8 100644 --- a/Makefile +++ b/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 +########################################################################## + +VERSION := 2.0.0 + +## usage + +.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" + +## development + +BUILT_PKG := out/$(notdir $(CURDIR)).zip + +node_modules/: package-lock.json; npm ci + +.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 "//" | sed "s[[[" +.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 -APPNAME = Jellyfin_Roku -VERSION = 1.6.6 +endif # ROKU_DEV_TARGET -ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\* +## sync branding -include app.mk +CONVERT_CMD ?= convert -gravity center +CONVERT_BLUEBG_CMD := $(CONVERT_CMD) -background "\#000b25" +BANNER := images/banner-dark.svg +ICON := images/icon-transparent.svg -dev: - $(MAKE) BUILD='dev' package +images/:; mkdir $@ -beta: - $(MAKE) BUILD='beta' package +.PHONY: redo # force rerun +$(BANNER) $(ICON): images/ redo + $(CURL_CMD) https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/$(@F) > $@ -release: - $(MAKE) BUILD='release' package +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 $< $@ -deploy: prep_staging remove install +.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 diff --git a/README.md b/README.md index 08f540703..0c6d5d9c2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app.mk b/app.mk deleted file mode 100644 index 9e09f21eb..000000000 --- a/app.mk +++ /dev/null @@ -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 ' 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 "//" | sed "s[[[" - -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 "//" | sed "s[[[" ; \ - else \ - curl -s -S -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "//" | sed "s[[[" ; \ - 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' - - - - diff --git a/bsconfig-prod.json b/bsconfig-prod.json new file mode 100644 index 000000000..7720b6167 --- /dev/null +++ b/bsconfig-prod.json @@ -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 +} diff --git a/bsconfig-tdd-sample.json b/bsconfig-tdd-sample.json new file mode 100644 index 000000000..ad0c7997d --- /dev/null +++ b/bsconfig-tdd-sample.json @@ -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 +} diff --git a/bsconfig-tests.json b/bsconfig-tests.json new file mode 100644 index 000000000..b84a3f065 --- /dev/null +++ b/bsconfig-tests.json @@ -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 +} diff --git a/bsconfig.json b/bsconfig.json index 4045d2b8d..0f1a18d18 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,19 +1,22 @@ { - "files": [ - "manifest", - "source/**/*.*", - "components/**/*.*", - "images/**/*.*", - "resources/**/*.*", - "locale/**/*.*", - "settings/*.*" - ], - "plugins": [ - "@rokucommunity/bslint" - ], - "diagnosticFilters": [ - "**/roku_modules/**/*", - "**/testFramework/*", - "**/tests/*" - ] -} \ No newline at end of file + "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 +} diff --git a/bsfmt.json b/bsfmt.json index ce0f85d4b..23cbd4f6e 100644 --- a/bsfmt.json +++ b/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/**/*.*" + ] +} \ No newline at end of file diff --git a/components/ButtonGroupHoriz.brs b/components/ButtonGroupHoriz.bs similarity index 100% rename from components/ButtonGroupHoriz.brs rename to components/ButtonGroupHoriz.bs diff --git a/components/ButtonGroupHoriz.xml b/components/ButtonGroupHoriz.xml index 8646a3be9..574e43fc1 100644 --- a/components/ButtonGroupHoriz.xml +++ b/components/ButtonGroupHoriz.xml @@ -1,9 +1,6 @@ - + - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/components/PersonDetails.brs b/components/PersonDetails.bs similarity index 98% rename from components/PersonDetails.brs rename to components/PersonDetails.bs index de3b57308..e0680bdb4 100644 --- a/components/PersonDetails.brs +++ b/components/PersonDetails.bs @@ -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") diff --git a/components/PersonDetails.xml b/components/PersonDetails.xml index 6acc1eb4c..fc7ead8ba 100644 --- a/components/PersonDetails.xml +++ b/components/PersonDetails.xml @@ -1,4 +1,4 @@ - + @@ -7,26 +7,26 @@ - - - - - - -
On this page

components_ButtonGroupHoriz.bs

sub init()
+    m.top.layoutDirection = "horiz"
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+    if key = "right"
+        i = m.top.buttonFocused
+        target = i + 1
+        if target >= m.top.getChildCount() then return false
+        m.top.focusButton = target
+        return true
+    else if key = "left"
+        i = m.top.buttonFocused
+        target = i - 1
+        if target < 0 then return false
+        m.top.focusButton = target
+        return true
+    else if key = "up" or key = "down"
+        m.top.escape = key
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ButtonGroupVert.bs.html b/docs/api/components_ButtonGroupVert.bs.html new file mode 100644 index 000000000..d89e3eff0 --- /dev/null +++ b/docs/api/components_ButtonGroupVert.bs.html @@ -0,0 +1,47 @@ +Source: components/ButtonGroupVert.bs
On this page

components_ButtonGroupVert.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_Buttons_JFButtons.bs.html b/docs/api/components_Buttons_JFButtons.bs.html new file mode 100644 index 000000000..6bc7eb3fa --- /dev/null +++ b/docs/api/components_Buttons_JFButtons.bs.html @@ -0,0 +1,125 @@ +Source: components/Buttons/JFButtons.bs
On this page

components_Buttons_JFButtons.bs

sub init()
+
+    m.top.focusable = true
+
+    m.menubg = m.top.findNode("menubg")
+
+    m.focusRing = m.top.findNode("focus")
+    m.buttonGroup = m.top.findNode("buttonGroup")
+    m.focusAnim = m.top.findNode("moveFocusAnimation")
+    m.focusAnimTranslation = m.top.findNode("focusLocation")
+    m.focusAnimWidth = m.top.findNode("focusWidth")
+    m.focusAnimHeight = m.top.findNode("focusHeight")
+
+    ' Set button color to global
+    m.focusRing.color = m.global.constants.colors.button
+
+    m.buttonCount = 0
+    m.selectedFocusedIndex = 0
+
+    m.textSizeTask = createObject("roSGNode", "TextSizeTask")
+
+    m.top.observeField("focusedChild", "focusChanged")
+    m.top.enableRenderTracking = true
+    m.top.observeField("renderTracking", "renderChanged")
+
+end sub
+
+'
+' When Selected Index set, ensure it is the one Focused
+sub selectedIndexChanged()
+    m.selectedFocusedIndex = m.top.selectedIndex
+end sub
+
+'
+' When options are fully displayed, set focus and selected option
+sub renderChanged()
+    if m.top.renderTracking = "full"
+        highlightSelected(m.selectedFocusedIndex, false)
+        m.top.setfocus(true)
+    end if
+end sub
+
+
+sub updateButtons()
+    m.textSizeTask.fontsize = 40
+    m.textSizeTask.text = m.top.buttons
+    m.textSizeTask.name = m.buttonCount
+    m.textSizeTask.observeField("width", "showButtons")
+    m.textSizeTask.control = "RUN"
+end sub
+
+sub showButtons()
+
+    totalWidth = 110 ' track for menu background width - start with side padding
+
+    for i = 0 to m.top.buttons.count() - 1
+        m.buttonCount = m.buttonCount + 1
+        l = m.buttonGroup.createChild("Label")
+        l.text = m.top.buttons[i]
+        l.font.size = 40
+        l.translation = [0, 10]
+        l.height = m.textSizeTask.height
+        l.width = m.textSizeTask.width[i] + 50
+        l.horizAlign = "center"
+        l.vertAlign = "center"
+        totalWidth = totalWidth + l.width + 45
+    end for
+
+    m.menubg.width = totalWidth
+    m.menubg.height = m.textSizeTask.height + 40
+
+end sub
+
+' Highlight selected menu option
+sub highlightSelected(index as integer, animate = true)
+
+    val = m.buttonGroup.getChild(index)
+    rect = val.ancestorBoundingRect(m.top)
+
+    if animate = true
+        m.focusAnimTranslation.keyValue = [m.focusRing.translation, [rect.x - 25, rect.y - 30]]
+        m.focusAnimWidth.keyValue = [m.focusRing.width, val.width + 50]
+        m.focusAnimHeight.keyValue = [m.focusRing.height, val.height + 60]
+        m.focusAnim.control = "start"
+    else
+        m.focusRing.translation = [rect.x - 25, rect.y - 30]
+        m.focusRing.width = val.width + 50
+        m.focusRing.height = val.height + 60
+    end if
+
+end sub
+
+' Change opacity of the highlighted menu item based on focus
+sub focusChanged()
+    if m.top.isInFocusChain()
+        m.focusRing.opacity = 1
+    else
+        m.focusRing.opacity = 0.6
+    end if
+end sub
+
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if not press then return false
+
+    if key = "left"
+        if m.selectedFocusedIndex > 0 then m.selectedFocusedIndex = m.selectedFocusedIndex - 1
+        highlightSelected(m.selectedFocusedIndex)
+        m.top.focusedIndex = m.selectedFocusedIndex
+        return true
+    else if key = "right"
+        if m.selectedFocusedIndex < m.buttonCount - 1 then m.selectedFocusedIndex = m.selectedFocusedIndex + 1
+        highlightSelected(m.selectedFocusedIndex)
+        m.top.focusedIndex = m.selectedFocusedIndex
+        return true
+    else if key = "OK"
+        m.top.selectedIndex = m.selectedFocusedIndex
+        return true
+    end if
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_Buttons_SlideOutButton.bs.html b/docs/api/components_Buttons_SlideOutButton.bs.html new file mode 100644 index 000000000..55ebc91a2 --- /dev/null +++ b/docs/api/components_Buttons_SlideOutButton.bs.html @@ -0,0 +1,103 @@ +Source: components/Buttons/SlideOutButton.bs
On this page

components_Buttons_SlideOutButton.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_Buttons_TextSizeTask.bs.html b/docs/api/components_Buttons_TextSizeTask.bs.html new file mode 100644 index 000000000..b20be0af8 --- /dev/null +++ b/docs/api/components_Buttons_TextSizeTask.bs.html @@ -0,0 +1,24 @@ +Source: components/Buttons/TextSizeTask.bs
On this page

components_Buttons_TextSizeTask.bs

sub init()
+    m.top.functionName = "getTextSize"
+end sub
+
+sub getTextSize()
+
+    reg = CreateObject("roFontRegistry")
+    font = reg.GetDefaultFont(m.top.fontsize, false, false)
+
+    res = []
+
+    for each line in m.top.text
+        res.push(font.GetOneLineWidth(line, m.top.maxWidth))
+    end for
+
+    m.top.height = font.GetOneLineHeight()
+
+
+    m.top.width = res
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_Clock.bs.html b/docs/api/components_Clock.bs.html new file mode 100644 index 000000000..9b09988cf --- /dev/null +++ b/docs/api/components_Clock.bs.html @@ -0,0 +1,42 @@ +Source: components/Clock.bs
On this page

components_Clock.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_GetNextEpisodeTask.bs.html b/docs/api/components_GetNextEpisodeTask.bs.html new file mode 100644 index 000000000..b38ca1ce2 --- /dev/null +++ b/docs/api/components_GetNextEpisodeTask.bs.html @@ -0,0 +1,19 @@ +Source: components/GetNextEpisodeTask.bs
On this page

components_GetNextEpisodeTask.bs

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.shows.GetEpisodes(m.top.showID, {
+        UserId: m.global.session.user.id,
+        StartItemId: m.top.videoID,
+        Limit: 2
+    })
+
+    m.top.nextEpisodeData = m.nextEpisodeData
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_GetPlaybackInfoTask.bs.html b/docs/api/components_GetPlaybackInfoTask.bs.html new file mode 100644 index 000000000..1df0bd196 --- /dev/null +++ b/docs/api/components_GetPlaybackInfoTask.bs.html @@ -0,0 +1,173 @@ +Source: components/GetPlaybackInfoTask.bs
On this page

components_GetPlaybackInfoTask.bs

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, startTimeTicks = 0 as longinteger)
+    currentView = m.global.sceneManager.callFunc("getActiveScene")
+    currentItem = m.global.queueManager.callFunc("getCurrentItem")
+
+    body = {
+        "DeviceProfile": getDeviceProfile()
+    }
+    params = {
+        "UserId": m.global.session.user.id,
+        "StartTimeTicks": currentItem.startingPoint,
+        "IsPlayback": true,
+        "AutoOpenLiveStream": true,
+        "MaxStreamingBitrate": "140000000",
+        "MaxStaticBitrate": "140000000",
+        "SubtitleStreamIndex": currentView.selectedSubtitle,
+        "MediaSourceId": currentItem.id,
+        "AudioStreamIndex": currentItem.selectedAudioStreamIndex
+    }
+
+    req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
+    req.SetRequest("POST")
+    return postJson(req, FormatJson(body))
+end function
+
+' Returns an array of playback info to be displayed during playback.
+' In the future, with a custom playback info view, we can return an associated array.
+sub getPlaybackInfoTask()
+    sessions = api.sessions.Get({ "deviceId": m.global.device.serverDeviceName })
+
+    m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
+
+    if isValid(sessions) and sessions.Count() > 0
+        m.top.data = { playbackInfo: GetTranscodingStats(sessions[0]) }
+    else
+        m.top.data = { playbackInfo: [tr("Unable to get playback information")] }
+    end if
+end sub
+
+function GetTranscodingStats(deviceSession)
+    sessionStats = { data: [] }
+
+    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>")
+            for each item in transcodingReasons
+                sessionStats.data.push("<b>โ€ข " + tr("Reason") + ":</b> " + item)
+            end for
+        end if
+
+        if isValid(videoCodec)
+            data = "<b>โ€ข " + tr("Video Codec") + ":</b> " + videoCodec
+            if deviceSession.TranscodingInfo.IsVideoDirect
+                data = data + " (" + tr("direct") + ")"
+            end if
+            sessionStats.data.push(data)
+        end if
+
+        if isValid(audioCodec)
+            data = "<b>โ€ข " + tr("Audio Codec") + ":</b> " + audioCodec
+            if deviceSession.TranscodingInfo.IsAudioDirect
+                data = data + " (" + tr("direct") + ")"
+            end if
+            sessionStats.data.push(data)
+        end if
+
+        if isValid(totalBitrate)
+            data = "<b>โ€ข " + tr("Total Bitrate") + ":</b> " + getDisplayBitrate(totalBitrate)
+            sessionStats.data.push(data)
+        end if
+
+        if isValid(audioChannels)
+            data = "<b>โ€ข " + tr("Audio Channels") + ":</b> " + Str(audioChannels)
+            sessionStats.data.push(data)
+        end if
+    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()
+        stream = m.playbackInfo.mediaSources[0].MediaStreams[0]
+        sessionStats.data.push("<header>" + tr("Stream Information") + "</header>")
+        if isValid(stream.Container)
+            data = "<b>โ€ข " + tr("Container") + ":</b> " + stream.Container
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.Size)
+            data = "<b>โ€ข " + tr("Size") + ":</b> " + stream.Size
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.BitRate)
+            data = "<b>โ€ข " + tr("Bit Rate") + ":</b> " + getDisplayBitrate(stream.BitRate)
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.Codec)
+            data = "<b>โ€ข " + tr("Codec") + ":</b> " + stream.Codec
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.CodecTag)
+            data = "<b>โ€ข " + tr("Codec Tag") + ":</b> " + stream.CodecTag
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.VideoRangeType)
+            data = "<b>โ€ข " + tr("Video range type") + ":</b> " + stream.VideoRangeType
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.PixelFormat)
+            data = "<b>โ€ข " + tr("Pixel format") + ":</b> " + stream.PixelFormat
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.Width) and isValid(stream.Height)
+            data = "<b>โ€ข " + tr("WxH") + ":</b> " + Str(stream.Width) + " x " + Str(stream.Height)
+            sessionStats.data.push(data)
+        end if
+        if isValid(stream.Level)
+            data = "<b>โ€ข " + tr("Level") + ":</b> " + Str(stream.Level)
+            sessionStats.data.push(data)
+        end if
+    end if
+
+    return sessionStats
+end function
+
+function havePlaybackInfo()
+    if not isValid(m.playbackInfo)
+        return false
+    end if
+
+    if not isValid(m.playbackInfo.mediaSources)
+        return false
+    end if
+
+    if m.playbackInfo.mediaSources.Count() <= 0
+        return false
+    end if
+
+    if not isValid(m.playbackInfo.mediaSources[0].MediaStreams)
+        return false
+    end if
+
+    if m.playbackInfo.mediaSources[0].MediaStreams.Count() <= 0
+        return false
+    end if
+
+    return true
+end function
+
+function getDisplayBitrate(bitrate)
+    if bitrate > 1000000
+        return Str(Fix(bitrate / 1000000)) + " Mbps"
+    else
+        return Str(Fix(bitrate / 1000)) + " Kbps"
+    end if
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_GetShuffleEpisodesTask.bs.html b/docs/api/components_GetShuffleEpisodesTask.bs.html new file mode 100644 index 000000000..ce5b7600b --- /dev/null +++ b/docs/api/components_GetShuffleEpisodesTask.bs.html @@ -0,0 +1,19 @@ +Source: components/GetShuffleEpisodesTask.bs
On this page

components_GetShuffleEpisodesTask.bs

import "pkg:/source/utils/config.bs"
+import "pkg:/source/api/sdk.bs"
+
+sub init()
+    m.top.functionName = "getShuffleEpisodesTask"
+end sub
+
+sub getShuffleEpisodesTask()
+    data = api.shows.GetEpisodes(m.top.showID, {
+        UserId: m.global.session.user.id,
+        SortBy: "Random",
+        Limit: 200
+    })
+
+    m.top.data = data
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_IconButton.bs.html b/docs/api/components_IconButton.bs.html new file mode 100644 index 000000000..4b060b41a --- /dev/null +++ b/docs/api/components_IconButton.bs.html @@ -0,0 +1,84 @@ +Source: components/IconButton.bs
On this page

components_IconButton.bs

sub init()
+    m.buttonBackground = m.top.findNode("buttonBackground")
+    m.buttonIcon = m.top.findNode("buttonIcon")
+    m.buttonText = m.top.findNode("buttonText")
+
+    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("focus", "onFocusChanged")
+end sub
+
+sub onFocusChanged()
+    if m.top.focus
+        m.buttonBackground.blendColor = m.top.focusBackground
+    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 = [((width - m.buttonIcon.width) / 2), ((height - m.buttonIcon.height) / 2)]
+        m.buttonText.translation = [0, height + 10]
+        m.buttonText.width = width
+    end if
+end sub
+
+sub onHeightChanged()
+    m.buttonBackground.height = m.top.height
+    setIconSize()
+end sub
+
+sub onWidthChanged()
+    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 = "right" and m.top.focus
+        m.top.escape = "right"
+    end if
+
+    if key = "left" and m.top.focus
+        m.top.escape = "left"
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_Alpha.bs.html b/docs/api/components_ItemGrid_Alpha.bs.html new file mode 100644 index 000000000..adef064f6 --- /dev/null +++ b/docs/api/components_ItemGrid_Alpha.bs.html @@ -0,0 +1,46 @@ +Source: components/ItemGrid/Alpha.bs
On this page

components_ItemGrid_Alpha.bs

sub init()
+    m.top.visible = true
+    m.Alphamenu = m.top.findNode("Alphamenu")
+    m.Alphamenu.focusable = true
+    m.Alphatext = m.top.findNode("alphatext")
+    m.focusedChild = m.top.findNode("focusedChild")
+    m.Alphamenu.focusedFont.size = 25
+    m.Alphamenu.font.size = 25
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if not press then return false
+
+    if key = "OK"
+        child = m.Alphatext.getChild(m.Alphamenu.itemFocused)
+
+        if child.title = m.top.itemAlphaSelected
+            m.top.itemAlphaSelected = ""
+            m.Alphamenu.focusFootprintBitmapUri = ""
+        else
+            m.Alphamenu.focusFootprintBitmapUri = "pkg:/images/white.png"
+            m.top.itemAlphaSelected = child.title
+        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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_FavoriteItemsTask.bs.html b/docs/api/components_ItemGrid_FavoriteItemsTask.bs.html new file mode 100644 index 000000000..5a93e86b8 --- /dev/null +++ b/docs/api/components_ItemGrid_FavoriteItemsTask.bs.html @@ -0,0 +1,22 @@ +Source: components/ItemGrid/FavoriteItemsTask.bs
On this page

components_ItemGrid_FavoriteItemsTask.bs

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
+
+sub setFavoriteStatus()
+
+    task = m.top.favTask
+
+    if task = "Favorite"
+        MarkItemFavorite(m.top.itemId)
+    else if task = "Unfavorite"
+        UnmarkItemFavorite(m.top.itemId)
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_GridItem.bs.html b/docs/api/components_ItemGrid_GridItem.bs.html new file mode 100644 index 000000000..1b2d2eb90 --- /dev/null +++ b/docs/api/components_ItemGrid_GridItem.bs.html @@ -0,0 +1,174 @@ +Source: components/ItemGrid/GridItem.bs
On this page

components_ItemGrid_GridItem.bs

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")
+    m.posterText = m.top.findNode("posterText")
+    m.itemText = m.top.findNode("itemText")
+    m.backdrop = m.top.findNode("backdrop")
+
+    m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
+
+    m.unplayedCount = m.top.findNode("unplayedCount")
+    m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
+
+    m.itemText.translation = [0, m.itemPoster.height + 7]
+
+    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
+    if m.itemText.visible then m.itemText.maxWidth = 250
+
+    'Parent is MarkupGrid and it's parent is the ItemGrid
+    m.topParent = m.top.GetParent().GetParent()
+    'Get the imageDisplayMode for these grid items
+    if isValid(m.topParent.imageDisplayMode)
+        m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
+    end if
+
+end sub
+
+sub itemContentChanged()
+
+    ' Set Random background colors from pallet
+    posterBackgrounds = m.global.constants.poster_bg_pallet
+    m.backdrop.blendColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
+
+    itemData = m.top.itemContent
+
+    if itemData = invalid then return
+
+    if itemData.type = "Movie"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Series"
+        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
+
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Boxset"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "TvChannel"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Folder"
+        m.itemPoster.uri = itemData.PosterUrl
+        'm.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+        m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
+    else if itemData.type = "Video"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Playlist"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Photo"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        m.itemText.text = itemData.Title
+    else if itemData.type = "Episode"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemIcon.uri = itemData.iconUrl
+        if isValid(itemData.json) and isValid(itemData.json.SeriesName)
+            m.itemText.text = itemData.json.SeriesName + " - " + itemData.Title
+        else
+            m.itemText.text = itemData.Title
+        end if
+    else if itemData.type = "MusicArtist"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemText.text = itemData.Title
+
+        m.itemPoster.height = 290
+        m.itemPoster.width = 290
+
+        m.itemText.translation = [0, m.itemPoster.height + 7]
+
+        m.backdrop.height = 290
+        m.backdrop.width = 290
+
+        m.posterText.height = 200
+        m.posterText.width = 280
+    else if isValid(itemData.json.type) and itemData.json.type = "MusicAlbum"
+        m.itemPoster.uri = itemData.PosterUrl
+        m.itemText.text = itemData.Title
+
+        m.itemPoster.height = 290
+        m.itemPoster.width = 290
+
+        m.itemText.translation = [0, m.itemPoster.height + 7]
+
+        m.backdrop.height = 290
+        m.backdrop.width = 290
+
+        m.posterText.height = 200
+        m.posterText.width = 280
+    else
+        m.log.warn("Unhandled Grid Item Type", itemData.type)
+    end if
+
+    'If Poster not loaded, ensure "blue box" is shown until loaded
+    if m.itemPoster.loadStatus <> "ready"
+        m.backdrop.visible = true
+        m.posterText.visible = true
+    end if
+
+    m.posterText.text = m.itemText.text
+
+end sub
+
+'
+'Use FocusPercent to animate scaling of Poser Image
+sub focusChanging()
+    scaleFactor = 0.85 + (m.top.focusPercent * 0.15)
+    m.posterMask.scale = [scaleFactor, scaleFactor]
+end sub
+
+'
+'Display or hide title Visibility on focus change
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        m.itemText.repeatCount = -1
+        m.posterMask.scale = [1, 1]
+    else
+        m.itemText.repeatCount = 0
+        if m.topParent.alphaActive = true
+            m.posterMask.scale = [0.85, 0.85]
+        end if
+    end if
+    if m.gridTitles = "showonhover"
+        m.itemText.visible = m.top.itemHasFocus
+    end if
+end sub
+
+'Hide backdrop and text when poster loaded
+sub onPosterLoadStatusChanged()
+    if m.itemPoster.loadStatus = "ready"
+        m.backdrop.visible = false
+        m.posterText.visible = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_GridItemSmall.bs.html b/docs/api/components_ItemGrid_GridItemSmall.bs.html new file mode 100644 index 000000000..8e9616ac5 --- /dev/null +++ b/docs/api/components_ItemGrid_GridItemSmall.bs.html @@ -0,0 +1,74 @@ +Source: components/ItemGrid/GridItemSmall.bs
On this page

components_ItemGrid_GridItemSmall.bs

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")
+    m.title = m.top.findNode("title")
+    m.posterText.font.size = 30
+    m.title.font.size = 25
+    m.backdrop = m.top.findNode("backdrop")
+
+    m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
+
+    'Parent is MarkupGrid and it's parent is the ItemGrid
+    m.topParent = m.top.GetParent().GetParent()
+
+    m.title.visible = false
+
+    'Get the imageDisplayMode for these grid items
+    if m.topParent.imageDisplayMode <> invalid
+        m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
+    end if
+end sub
+
+sub itemContentChanged()
+    m.backdrop.blendColor = "#101010"
+
+    m.title.visible = false
+
+    if isValid(m.topParent.showItemTitles)
+        if LCase(m.topParent.showItemTitles) = "showalways"
+            m.title.visible = true
+        end if
+    end if
+
+    itemData = m.top.itemContent
+
+    if not isValid(itemData) then return
+
+    m.itemPoster.uri = itemData.PosterUrl
+    m.posterText.text = itemData.title
+    m.title.text = itemData.title
+
+    'If Poster not loaded, ensure "blue box" is shown until loaded
+    if m.itemPoster.loadStatus <> "ready"
+        m.backdrop.visible = true
+        m.posterText.visible = true
+    end if
+end sub
+
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        m.title.repeatCount = -1
+    else
+        m.title.repeatCount = 0
+    end if
+
+    if isValid(m.topParent.showItemTitles)
+        if LCase(m.topParent.showItemTitles) = "showonhover"
+            m.title.visible = m.top.itemHasFocus
+        end if
+    end if
+end sub
+
+'Hide backdrop and text when poster loaded
+sub onPosterLoadStatusChanged()
+    if m.itemPoster.loadStatus = "ready"
+        m.backdrop.visible = false
+        m.posterText.visible = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_ItemGrid.bs.html b/docs/api/components_ItemGrid_ItemGrid.bs.html new file mode 100644 index 000000000..2560acc7a --- /dev/null +++ b/docs/api/components_ItemGrid_ItemGrid.bs.html @@ -0,0 +1,904 @@ +Source: components/ItemGrid/ItemGrid.bs
On this page

components_ItemGrid_ItemGrid.bs

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 = m.global.session.user.settings["itemgrid.showItemCount"]
+
+    m.tvGuide = invalid
+    m.channelFocused = invalid
+
+    m.itemGrid = m.top.findNode("itemGrid")
+    m.backdrop = m.top.findNode("backdrop")
+    m.newBackdrop = m.top.findNode("backdropTransition")
+    m.emptyText = m.top.findNode("emptyText")
+
+    m.genreList = m.top.findNode("genrelist")
+    m.genreList.observeField("itemSelected", "onGenreItemSelected")
+    m.genreData = CreateObject("roSGNode", "ContentNode")
+    m.genreList.content = m.genreData
+
+    m.swapAnimation = m.top.findNode("backroundSwapAnimation")
+    m.swapAnimation.observeField("state", "swapDone")
+
+    m.loadedRows = 0
+    m.loadedItems = 0
+
+    m.data = CreateObject("roSGNode", "ContentNode")
+
+    m.itemGrid.content = m.data
+    m.itemGrid.setFocus(true)
+
+    m.itemGrid.observeField("itemFocused", "onItemFocused")
+    m.itemGrid.observeField("itemSelected", "onItemSelected")
+    m.itemGrid.observeField("alphaSelected", "onItemalphaSelected")
+
+    'Voice filter setup
+    m.voiceBox = m.top.findNode("voiceBox")
+    m.voiceBox.voiceEnabled = true
+    m.voiceBox.active = true
+    m.voiceBox.observeField("text", "onvoiceFilter")
+    'set voice help text
+    m.voiceBox.hintText = tr("Use voice remote to search")
+
+    'backdrop
+    m.newBackdrop.observeField("loadStatus", "newBGLoaded")
+
+    'Background Image Queued for loading
+    m.queuedBGUri = ""
+
+    'Item sort - maybe load defaults from user prefs?
+    m.sortField = "SortName"
+    m.sortAscending = true
+
+    m.filter = "All"
+    m.favorite = "Favorite"
+
+    m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
+
+    'set inital counts for overhang before content is loaded.
+    m.loadItemsTask.totalRecordCount = 0
+
+    m.Alpha = m.top.findNode("AlphaMenu")
+    m.AlphaSelected = m.top.findNode("AlphaSelected")
+
+    'Get reset folder setting
+    m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
+
+    m.micButton = m.top.findNode("micButton")
+    m.micButtonText = m.top.findNode("micButtonText")
+    'Hide voice search if device does not have voice remote
+    if m.global.device.hasVoiceRemote = false
+        m.micButton.visible = false
+        m.micButtonText.visible = false
+    end if
+end sub
+
+'
+'Genre Item Selected
+sub onGenreItemSelected()
+    m.top.selectedItem = m.genreList.content.getChild(m.genreList.rowItemSelected[0]).getChild(m.genreList.rowItemSelected[1])
+end sub
+
+'
+'Load initial set of Data
+sub loadInitialItems()
+    m.loadItemsTask.control = "stop"
+    startLoadingSpinner()
+
+    if m.top.parentItem.json.Type = "CollectionFolder" 'or m.top.parentItem.json.Type = "Folder"
+        m.top.HomeLibraryItem = m.top.parentItem.Id
+    end if
+
+    if m.top.parentItem.backdropUrl <> invalid
+        SetBackground(m.top.parentItem.backdropUrl)
+    end if
+
+    ' Read view/sort/filter settings
+    if m.top.parentItem.collectionType = "livetv"
+        ' Translate between app and server nomenclature
+        viewSetting = m.global.session.user.settings["display.livetv.landing"]
+        'Move mic to be visiable on TV Guide screen
+        if m.global.device.hasVoiceRemote = true
+            m.micButton.translation = "[1540, 92]"
+            m.micButtonText.visible = true
+            m.micButtonText.translation = "[1600,130]"
+            m.micButtonText.font.size = 22
+            m.micButtonText.text = tr("Search")
+        end if
+        if viewSetting = "guide"
+            m.view = "tvGuide"
+        else
+            m.view = "livetv"
+        end if
+        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 = 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 = 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.filter = invalid then m.filter = "All"
+
+    if sortAscendingStr = invalid or sortAscendingStr = true
+        m.sortAscending = true
+    else
+        m.sortAscending = false
+    end if
+    ' Set Studio Id
+    if m.top.parentItem.json.type = "Studio"
+        m.loadItemsTask.studioIds = m.top.parentItem.id
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+        m.loadItemsTask.genreIds = ""
+        ' Set Genre Id
+    else if m.top.parentItem.json.type = "Genre"
+        m.loadItemsTask.genreIds = m.top.parentItem.id
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+        m.loadItemsTask.studioIds = ""
+    else if (m.view = "Shows" or m.options.view = "Shows") or (m.view = "Movies" or m.options.view = "Movies")
+        m.loadItemsTask.studioIds = ""
+        m.loadItemsTask.genreIds = ""
+    else
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    end if
+    updateTitle()
+
+    m.loadItemsTask.nameStartsWith = m.top.alphaSelected
+    m.loadItemsTask.searchTerm = m.voiceBox.text
+    m.emptyText.visible = false
+    m.loadItemsTask.sortField = m.sortField
+    m.loadItemsTask.sortAscending = m.sortAscending
+    m.loadItemsTask.filter = m.filter
+    m.loadItemsTask.startIndex = 0
+
+    ' Load Item Types
+    if getCollectionType() = "movies"
+        m.loadItemsTask.itemType = "Movie"
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    else if getCollectionType() = "tvshows"
+        m.loadItemsTask.itemType = "Series"
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    else if getCollectionType() = "music"
+        ' Default Settings
+        m.loadItemsTask.recursive = true
+        m.itemGrid.itemSize = "[290, 290]"
+
+        m.loadItemsTask.itemType = "MusicArtist"
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+
+        m.view = m.global.session.user.settings["display.music.view"]
+
+        if m.view = "music-album"
+            m.loadItemsTask.itemType = "MusicAlbum"
+        end if
+    else if m.top.parentItem.collectionType = "livetv"
+        m.loadItemsTask.itemType = "TvChannel"
+        m.loadItemsTask.itemId = " "
+        ' For LiveTV, we want to "Fit" the item images, not zoom
+        m.top.imageDisplayMode = "scaleToFit"
+
+        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"
+        if m.voiceBox.text <> ""
+            m.loadItemsTask.recursive = true
+        else
+            ' non recursive for collections (folders, boxsets, photo albums, etc)
+            m.loadItemsTask.recursive = false
+        end if
+    else if m.top.parentItem.json.type = "Studio"
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+        m.loadItemsTask.itemType = "Series,Movie"
+        m.top.imageDisplayMode = "scaleToFit"
+    else if m.top.parentItem.json.type = "Genre"
+        m.loadItemsTask.itemType = "Series,Movie"
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+    else
+        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")
+        m.loadItemsTask.view = "Networks"
+        m.top.imageDisplayMode = "scaleToFit"
+    else if m.top.parentItem.type <> "Folder" and (m.options.view = "Genres" or m.view = "Genres")
+        m.loadItemsTask.StudioIds = m.top.parentItem.Id
+        m.loadItemsTask.view = "Genres"
+    else if m.top.parentItem.type <> "Folder" and (m.options.view = "Shows" or m.view = "Shows")
+        m.loadItemsTask.studioIds = ""
+        m.loadItemsTask.view = "Shows"
+    else if m.top.parentItem.type <> "Folder" and (m.options.view = "Movies" or m.view = "Movies")
+        m.loadItemsTask.studioIds = ""
+        m.loadItemsTask.view = "Movies"
+    end if
+
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    startLoadingSpinner(false)
+    m.loadItemsTask.control = "RUN"
+    SetUpOptions()
+end sub
+
+' Set Movies view, sort, and filter options
+sub setMoviesOptions(options)
+    options.views = [
+        { "Title": tr("Movies"), "Name": "Movies" },
+        { "Title": tr("Studios"), "Name": "Studios" },
+        { "Title": tr("Genres"), "Name": "Genres" }
+    ]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
+        { "Title": tr("CRITIC_RATING"), "Name": "CriticRating" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
+        { "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+        { "Title": tr("RUNTIME"), "Name": "Runtime" }
+    ]
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" },
+        { "Title": tr("Played"), "Name": "Played" },
+        { "Title": tr("Unplayed"), "Name": "Unplayed" },
+        { "Title": tr("Resumable"), "Name": "Resumable" }
+    ]
+end sub
+
+' Set Boxset view, sort, and filter options
+sub setBoxsetsOptions(options)
+    options.views = [{ "Title": tr("Shows"), "Name": "shows" }]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+    ]
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" },
+        { "Title": tr("Played"), "Name": "Played" },
+        { "Title": tr("Unplayed"), "Name": "Unplayed" }
+    ]
+end sub
+
+' Set TV Show view, sort, and filter options
+sub setTvShowsOptions(options)
+    options.views = [
+        { "Title": tr("Shows"), "Name": "Shows" },
+        { "Title": tr("Networks"), "Name": "Networks" },
+        { "Title": tr("Genres"), "Name": "Genres" }
+
+    ]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+    ]
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" },
+        { "Title": tr("Played"), "Name": "Played" },
+        { "Title": tr("Unplayed"), "Name": "Unplayed" }
+    ]
+
+    if isValid(m.view)
+        if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
+            options.sort = [{ "Title": tr("TITLE"), "Name": "SortName" }]
+            options.filter = []
+        end if
+    end if
+
+end sub
+
+' Set Live TV view, sort, and filter options
+sub setLiveTvOptions(options)
+    options.views = [
+        { "Title": tr("Channels"), "Name": "livetv" },
+        { "Title": tr("TV Guide"), "Name": "tvGuide" }
+    ]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" }
+    ]
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" }
+    ]
+    options.favorite = [
+        { "Title": tr("Favorite"), "Name": "Favorite" }
+    ]
+end sub
+
+' Set Music view, sort, and filter options
+sub setMusicOptions(options)
+    options.views = [
+        { "Title": tr("Artists"), "Name": "music-artist" },
+        { "Title": tr("Albums"), "Name": "music-album" },
+    ]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+    ]
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" }
+    ]
+end sub
+
+' Set Photo Album view, sort, and filter options
+sub setPhotoAlbumOptions(options)
+    options.views = [
+        { "Title": tr("Slideshow Off"), "Name": "singlephoto" }
+        { "Title": tr("Slideshow On"), "Name": "slideshowphoto" }
+        { "Title": tr("Random Off"), "Name": "singlephoto" }
+        { "Title": tr("Random On"), "Name": "randomphoto" }
+    ]
+    options.sort = []
+    options.filter = []
+end sub
+
+' Set Default view, sort, and filter options
+sub setDefaultOptions(options)
+    options.views = [
+        { "Title": tr("Default"), "Name": "default" }
+    ]
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" }
+    ]
+end sub
+
+' Return parent collection type
+function getCollectionType() as string
+    if m.top.parentItem.collectionType = invalid
+        return m.top.parentItem.Type
+    else
+        return m.top.parentItem.CollectionType
+    end if
+end function
+
+' Search string array for search value. Return if it's found
+function inStringArray(array, searchValue) as boolean
+    for each item in array
+        if lcase(item) = lcase(searchValue) then return true
+    end for
+    return false
+end function
+
+' Data to display when options button selected
+sub SetUpOptions()
+    options = {}
+    options.filter = []
+    options.favorite = []
+
+    if getCollectionType() = "movies"
+        setMoviesOptions(options)
+    else if inStringArray(["boxsets", "Boxset"], getCollectionType())
+        setBoxsetsOptions(options)
+    else if getCollectionType() = "tvshows"
+        setTvShowsOptions(options)
+    else if getCollectionType() = "livetv"
+        setLiveTvOptions(options)
+    else if inStringArray(["photoalbum", "photo", "homevideos"], getCollectionType())
+        setPhotoAlbumOptions(options)
+    else if getCollectionType() = "music"
+        setMusicOptions(options)
+
+    else
+        setDefaultOptions(options)
+    end if
+
+    ' Set selected view option
+    for each o in options.views
+        if o.Name = m.view
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.view = o.Name
+        end if
+    end for
+
+    ' Set selected sort option
+    for each o in options.sort
+        if o.Name = m.sortField
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.sortField = o.Name
+        end if
+    end for
+
+    ' Set selected filter option
+    for each o in options.filter
+        if o.Name = m.filter
+            o.Selected = true
+            m.options.filter = o.Name
+        end if
+    end for
+
+    m.options.options = options
+end sub
+
+
+'
+'Handle loaded data, and add to Grid
+sub ItemDataLoaded(msg)
+    stopLoadingSpinner()
+    m.top.alphaActive = false
+    itemData = msg.GetData()
+    m.loadItemsTask.unobserveField("content")
+    m.loadItemsTask.content = []
+
+    if itemData = invalid
+        m.Loading = false
+        return
+    end if
+
+    if m.loadItemsTask.view = "Genres"
+        ' Reset genre list data
+        m.genreData.removeChildren(m.genreData.getChildren(-1, 0))
+
+        for each item in itemData
+            m.genreData.appendChild(item)
+        end for
+
+        m.itemGrid.opacity = "0"
+        m.genreList.opacity = "1"
+
+        m.itemGrid.setFocus(false)
+        m.genreList.setFocus(true)
+
+        m.loading = false
+        stopLoadingSpinner()
+        return
+    end if
+
+    for each item in itemData
+        m.data.appendChild(item)
+    end for
+
+    m.itemGrid.opacity = "1"
+    m.genreList.opacity = "0"
+
+    'Update the stored counts
+    m.loadedItems = m.itemGrid.content.getChildCount()
+    m.loadedRows = m.loadedItems / m.itemGrid.numColumns
+    m.Loading = false
+    'If there are no items to display, show message
+    if m.loadedItems = 0
+        m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
+        m.emptyText.visible = true
+    end if
+
+    m.itemGrid.setFocus(true)
+    m.genreList.setFocus(false)
+    stopLoadingSpinner()
+end sub
+
+'
+'Set Background Image
+sub SetBackground(backgroundUri as string)
+
+    'If a new image is being loaded, or transitioned to, store URL to load next
+    if m.swapAnimation.state <> "stopped" or m.newBackdrop.loadStatus = "loading"
+        m.queuedBGUri = backgroundUri
+        return
+    end if
+
+    m.newBackdrop.uri = backgroundUri
+end sub
+
+'
+'Handle new item being focused
+sub onItemFocused()
+
+    focusedRow = m.itemGrid.currFocusRow
+
+    itemInt = m.itemGrid.itemFocused
+
+    updateTitle()
+
+    ' If no selected item, set background to parent backdrop
+    if itemInt = -1
+        return
+    end if
+
+    m.selectedFavoriteItem = m.itemGrid.content.getChild(m.itemGrid.itemFocused)
+
+    ' Set Background to item backdrop
+    SetBackground(m.itemGrid.content.getChild(m.itemGrid.itemFocused).backdropUrl)
+
+    ' Load more data if focus is within last 5 rows, and there are more items to load
+    if focusedRow >= m.loadedRows - 5 and m.loadeditems < m.loadItemsTask.totalRecordCount
+        loadMoreData()
+    end if
+end sub
+
+'
+'When Image Loading Status changes
+sub newBGLoaded()
+    'If image load was sucessful, start the fade swap
+    if m.newBackdrop.loadStatus = "ready"
+        m.swapAnimation.control = "start"
+    end if
+end sub
+
+'
+'Swap Complete
+sub swapDone()
+
+    if m.swapAnimation.state = "stopped"
+
+        'Set main BG node image and hide transitioning node
+        m.backdrop.uri = m.newBackdrop.uri
+        m.backdrop.opacity = 0.25
+        m.newBackdrop.opacity = 0
+
+        'If there is another one to load
+        if m.newBackdrop.uri <> m.queuedBGUri and m.queuedBGUri <> ""
+            SetBackground(m.queuedBGUri)
+            m.queuedBGUri = ""
+        end if
+    end if
+end sub
+
+'
+'Load next set of items
+sub loadMoreData()
+    if m.Loading = true then return
+
+    startLoadingSpinner(false)
+    m.Loading = true
+    m.loadItemsTask.startIndex = m.loadedItems
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    m.loadItemsTask.control = "RUN"
+end sub
+
+'
+'Item Selected
+sub onItemSelected()
+    m.top.selectedItem = m.itemGrid.content.getChild(m.itemGrid.itemSelected)
+end sub
+
+sub onItemalphaSelected()
+    if m.top.alphaSelected <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        m.loadItemsTask.searchTerm = ""
+        m.VoiceBox.text = ""
+        m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
+        startLoadingSpinner(false)
+        loadInitialItems()
+    end if
+end sub
+
+sub onvoiceFilter()
+    if m.VoiceBox.text <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        m.top.alphaSelected = ""
+        m.loadItemsTask.NameStartsWith = " "
+        m.loadItemsTask.searchTerm = m.voiceBox.text
+        m.loadItemsTask.recursive = true
+        startLoadingSpinner(false)
+        loadInitialItems()
+    end if
+end sub
+
+
+'
+'Check if options updated and any reloading required
+sub optionsClosed()
+    if m.top.parentItem.collectionType = "livetv" and m.options.view <> m.view
+        if m.options.view = "tvGuide"
+            m.view = "tvGuide"
+            set_user_setting("display.livetv.landing", "guide")
+            showTVGuide()
+            return
+        else
+            m.view = "livetv"
+            set_user_setting("display.livetv.landing", "channels")
+
+            if m.tvGuide <> invalid
+                ' Try to hide the TV Guide
+                m.top.removeChild(m.tvGuide)
+            end if
+        end if
+    end if
+
+    if m.top.parentItem.Type = "CollectionFolder" or m.top.parentItem.Type = "Folder" or m.top.parentItem.CollectionType = "CollectionFolder"
+        ' Did the user just request "Random" on a PhotoAlbum?
+        if m.options.view = "singlephoto"
+            set_user_setting("photos.slideshow", "false")
+            set_user_setting("photos.random", "false")
+        else if m.options.view = "slideshowphoto"
+            set_user_setting("photos.slideshow", "true")
+            set_user_setting("photos.random", "false")
+        else if m.options.view = "randomphoto"
+            set_user_setting("photos.random", "true")
+            set_user_setting("photos.slideshow", "false")
+        end if
+    end if
+
+    reload = false
+
+    if m.top.parentItem.collectionType = "music"
+        if m.options.view <> m.view
+            m.view = m.options.view
+            set_user_setting("display.music.view", m.view)
+            reload = true
+        end if
+    else
+        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
+            set_user_setting("display." + m.top.parentItem.Id + ".landing", m.view)
+            reload = true
+        end if
+    end if
+
+    if m.options.sortField <> m.sortField or m.options.sortAscending <> m.sortAscending
+        m.sortField = m.options.sortField
+        m.sortAscending = m.options.sortAscending
+        reload = true
+
+        'Store sort settings
+        if m.sortAscending = true
+            sortAscendingStr = "true"
+        else
+            sortAscendingStr = "false"
+        end if
+
+        if m.top.parentItem.collectionType = "livetv"
+            set_user_setting("display.livetv.sortField", m.sortField)
+            set_user_setting("display.livetv.sortAscending", sortAscendingStr)
+        else
+            set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
+            set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", sortAscendingStr)
+        end if
+    end if
+    if m.options.filter <> m.filter
+        m.filter = m.options.filter
+        updateTitle()
+        reload = true
+        'Store filter setting
+        if m.top.parentItem.collectionType = "livetv"
+            set_user_setting("display.livetv.filter", m.options.filter)
+        else
+            set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
+        end if
+    end if
+    if reload
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        loadInitialItems()
+    end if
+
+    m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+    m.genreList.setFocus(m.genreList.opacity = 1)
+
+    if m.tvGuide <> invalid
+        m.tvGuide.lastFocus.setFocus(true)
+    end if
+
+end sub
+
+sub showTVGuide()
+    if m.tvGuide = invalid
+        m.tvGuide = createObject("roSGNode", "Schedule")
+        m.top.signalBeacon("EPGLaunchInitiate") ' Required Roku Performance monitoring
+        m.tvGuide.observeField("watchChannel", "onChannelSelected")
+        m.tvGuide.observeField("focusedChannel", "onChannelFocused")
+    end if
+    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
+
+sub onChannelSelected(msg)
+    node = msg.getRoSGNode()
+    m.top.lastFocus = lastFocusedChild(node)
+    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
+
+sub onChannelFocused(msg)
+    node = msg.getRoSGNode()
+    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
+
+    if m.itemGrid.opacity = 1
+        topGrp = m.itemGrid
+    else
+        topGrp = m.genreList
+    end if
+    searchGrp = m.top.findNode("voiceBox")
+
+    if key = "left" and searchGrp.isinFocusChain()
+        topGrp.setFocus(true)
+        searchGrp.setFocus(false)
+    end if
+
+    if key = "options"
+        if m.options.visible = true
+            m.options.visible = false
+            m.top.removeChild(m.options)
+            optionsClosed()
+        else
+            channelSelected = m.channelFocused
+            itemSelected = m.selectedFavoriteItem
+            if itemSelected <> invalid
+                m.options.selectedFavoriteItem = itemSelected
+            end if
+            if channelSelected <> invalid
+                if channelSelected.type = "TvChannel"
+                    m.options.selectedFavoriteItem = channelSelected
+                end if
+            end if
+            m.options.visible = true
+            m.top.appendChild(m.options)
+            m.options.setFocus(true)
+        end if
+        return true
+    else if key = "back"
+        if m.options.visible = true
+            m.options.visible = false
+            optionsClosed()
+            return true
+        else
+            m.global.sceneManager.callfunc("popScene")
+            m.loadItemsTask.control = "stop"
+            return true
+        end if
+    else if key = "OK"
+        markupGrid = m.top.findNode("itemGrid")
+        itemToPlay = getItemFocused()
+
+        if itemToPlay <> invalid and itemToPlay.type = "Photo"
+            ' Spawn photo player task
+            photoPlayer = CreateObject("roSgNode", "PhotoDetails")
+            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)
+        alpha = m.alpha.getChild(0).findNode("Alphamenu")
+        alpha.setFocus(true)
+        return true
+
+    else if key = "right" and m.Alpha.isinFocusChain()
+        m.top.alphaActive = false
+        m.Alpha.setFocus(false)
+        m.Alpha.visible = true
+        topGrp.setFocus(true)
+        return true
+    else if key = "replay" and topGrp.isinFocusChain()
+        if m.resetGrid = true
+            m.itemGrid.animateToItem = 0
+        else
+            m.itemGrid.jumpToItem = 0
+        end if
+    end if
+
+    if key = "replay"
+        m.loadItemsTask.searchTerm = ""
+        m.loadItemsTask.nameStartsWith = ""
+        m.voiceBox.text = ""
+        m.top.alphaSelected = ""
+        m.loadItemsTask.filter = "All"
+        m.filter = "All"
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        loadInitialItems()
+        return true
+    end if
+
+    return false
+end function
+
+sub updateTitle()
+    m.top.overhangTitle = m.top.parentItem.title
+
+    if m.filter = "Favorites"
+        m.top.overhangTitle = m.top.parentItem.title + " " + tr("(Favorites)")
+    end if
+
+    if m.voiceBox.text <> ""
+        m.top.overhangTitle = m.top.parentItem.title + tr(" (Filtered by ") + m.loadItemsTask.searchTerm + ")"
+    end if
+
+    if m.top.alphaSelected <> ""
+        m.top.overhangTitle = m.top.parentItem.title + tr(" (Filtered by ") + m.loadItemsTask.nameStartsWith + ")"
+    end if
+
+    if m.view = "music-artist"
+        m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Artists"))
+    else if m.view = "music-album"
+        m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Albums"))
+    end if
+
+    if m.options.view = "Networks" or m.view = "Networks"
+        m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Networks"))
+    end if
+
+    if m.options.view = "Studios" or m.view = "Studios"
+        m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Studios"))
+    end if
+
+    if m.options.view = "Genres" or m.view = "Genres"
+        m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Genres"))
+    end if
+
+    actInt = m.itemGrid.itemFocused + 1
+
+    if m.showItemCount and m.loadItemsTask.totalRecordCount > 0 and m.options.view <> "Genres" and m.view <> "Genres"
+        m.top.overhangTitle += " (" + tr("%1 of %2").Replace("%1", actInt.toStr()).Replace("%2", m.loadItemsTask.totalRecordCount.toStr()) + ")"
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_ItemGridOptions.bs.html b/docs/api/components_ItemGrid_ItemGridOptions.bs.html new file mode 100644 index 000000000..0dc909520 --- /dev/null +++ b/docs/api/components_ItemGrid_ItemGridOptions.bs.html @@ -0,0 +1,391 @@ +Source: components/ItemGrid/ItemGridOptions.bs
On this page

components_ItemGrid_ItemGridOptions.bs

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
+    m.buttons.setFocus(true)
+
+    m.favoriteMenu = m.top.findNode("favoriteMenu")
+    m.selectedFavoriteItem = m.top.findNode("selectedFavoriteItem")
+
+    m.selectedSortIndex = 0
+    m.selectedItem = 1
+
+    m.menus = []
+    m.menus.push(m.top.findNode("viewMenu"))
+    m.menus.push(m.top.findNode("sortMenu"))
+    m.menus.push(m.top.findNode("filterMenu"))
+
+    m.filterOptions = m.top.findNode("filterOptions")
+
+    m.filterMenu = m.top.findNode("filterMenu")
+    m.filterMenu.observeField("itemFocused", "onFilterFocusChange")
+
+    m.viewNames = []
+    m.sortNames = []
+    m.filterNames = []
+
+    ' Animation
+    m.fadeAnim = m.top.findNode("fadeAnim")
+    m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
+    m.fadeInAnimOpacity = m.top.findNode("inOpacity")
+    m.showChecklistAnimation = m.top.findNode("showChecklistAnimation")
+    m.hideChecklistAnimation = m.top.findNode("hideChecklistAnimation")
+
+    m.buttons.observeField("focusedIndex", "buttonFocusChanged")
+    m.favoriteMenu.observeField("buttonSelected", "toggleFavorite")
+end sub
+
+sub showChecklist()
+    if m.filterOptions.opacity = 0
+        if m.showChecklistAnimation.state = "stopped"
+            m.showChecklistAnimation.control = "start"
+        end if
+    end if
+end sub
+
+sub hideChecklist()
+    if m.filterOptions.opacity = 1
+        if m.hideChecklistAnimation.state = "stopped"
+            m.hideChecklistAnimation.control = "start"
+        end if
+    end if
+end sub
+
+sub onFilterFocusChange()
+    if not isFilterMenuDataValid()
+        hideChecklist()
+        return
+    end if
+
+    if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
+        showChecklist()
+    else
+        hideChecklist()
+    end if
+
+    m.filterOptions.content = m.filterMenu.content.getChild(m.filterMenu.itemFocused)
+    if isValid(m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState)
+        m.filterOptions.checkedState = m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState
+    else
+        m.filterOptions.checkedState = []
+    end if
+end sub
+
+' 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
+    if m.top.options.views <> invalid
+        viewContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        selectedViewIndex = m.selectedViewIndex
+
+        for each view in m.top.options.views
+            entry = viewContent.CreateChild("ContentNode")
+            entry.title = view.Title
+            m.viewNames.push(view.Name)
+            if (view.selected <> invalid and view.selected = true) or viewContent.Name = m.top.view
+                selectedViewIndex = index
+            end if
+            index = index + 1
+        end for
+        m.menus[0].content = viewContent
+        m.menus[0].checkedItem = selectedViewIndex
+    end if
+
+    ' Sort Tab
+    if m.top.options.sort <> invalid
+        sortContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        m.selectedSortIndex = 0
+
+        for each sortItem in m.top.options.sort
+            entry = sortContent.CreateChild("ContentNode")
+            entry.title = sortItem.Title
+            m.sortNames.push(sortItem.Name)
+            if sortItem.Selected <> invalid and sortItem.Selected = true
+                m.selectedSortIndex = index
+                if sortItem.Ascending <> invalid and sortItem.Ascending = false
+                    m.top.sortAscending = 0
+                else
+                    m.top.sortAscending = 1
+                end if
+            end if
+            index = index + 1
+        end for
+        m.menus[1].content = sortContent
+        m.menus[1].checkedItem = m.selectedSortIndex
+
+        if m.top.sortAscending = 1
+            m.menus[1].focusedCheckedIconUri = m.global.constants.icons.ascending_black
+            m.menus[1].checkedIconUri = m.global.constants.icons.ascending_white
+        else
+            m.menus[1].focusedCheckedIconUri = m.global.constants.icons.descending_black
+            m.menus[1].checkedIconUri = m.global.constants.icons.descending_white
+        end if
+    end if
+
+    ' Filter Tab
+    if m.top.options.filter <> invalid
+        filterContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        m.selectedFilterIndex = 0
+
+        for each filterItem in m.top.options.filter
+            entry = filterContent.CreateChild("OptionNode")
+            entry.title = filterItem.Title
+            entry.name = filterItem.Name
+            entry.delimiter = filterItem.Delimiter
+
+            if isValid(filterItem.options)
+                for each filterItemOption in filterItem.options
+                    entryOption = entry.CreateChild("ContentNode")
+                    entryOption.title = toString(filterItemOption)
+                end for
+                entry.checkedState = filterItem.checkedState
+            end if
+
+            m.filterNames.push(filterItem.Name)
+            if filterItem.selected <> invalid and filterItem.selected = true
+                m.selectedFilterIndex = index
+            end if
+            index = index + 1
+        end for
+        m.menus[2].content = filterContent
+        m.menus[2].checkedItem = m.selectedFilterIndex
+    else
+        filterContent = CreateObject("roSGNode", "ContentNode")
+        entry = filterContent.CreateChild("ContentNode")
+        entry.title = "All"
+        m.filterNames.push("All")
+        m.menus[2].content = filterContent
+        m.menus[2].checkedItem = 0
+    end if
+end sub
+
+' Switch menu shown when button focus changes
+sub buttonFocusChanged()
+    if m.buttons.focusedIndex = m.selectedItem
+        if m.buttons.hasFocus()
+            m.buttons.setFocus(false)
+            m.menus[m.selectedItem].setFocus(false)
+            m.menus[m.selectedItem].visible = false
+            m.favoriteMenu.setFocus(true)
+        end if
+    end if
+    m.fadeOutAnimOpacity.fieldToInterp = m.menus[m.selectedItem].id + ".opacity"
+    m.fadeInAnimOpacity.fieldToInterp = m.menus[m.buttons.focusedIndex].id + ".opacity"
+    m.fadeAnim.control = "start"
+    m.selectedItem = m.buttons.focusedIndex
+end sub
+
+sub toggleFavorite()
+    m.favItemsTask = createObject("roSGNode", "FavoriteItemsTask")
+    if m.favoriteMenu.iconUri = "pkg:/images/icons/favorite.png"
+        m.favoriteMenu.iconUri = "pkg:/images/icons/favorite_selected.png"
+        m.favoriteMenu.focusedIconUri = "pkg:/images/icons/favorite_selected.png"
+        ' Run the task to actually favorite it via API
+        m.favItemsTask.favTask = "Favorite"
+        m.favItemsTask.itemId = m.selectedFavoriteItem.id
+        m.favItemsTask.control = "RUN"
+    else
+        m.favoriteMenu.iconUri = "pkg:/images/icons/favorite.png"
+        m.favoriteMenu.focusedIconUri = "pkg:/images/icons/favorite.png"
+        m.favItemsTask.favTask = "Unfavorite"
+        m.favItemsTask.itemId = m.selectedFavoriteItem.id
+        m.favItemsTask.control = "RUN"
+    end if
+    ' Make sure we set the Favorite Heart color for the appropriate child
+    setHeartColor("#cc3333")
+end sub
+
+sub setHeartColor(color as string)
+    try
+        for i = 0 to 6
+            node = m.favoriteMenu.getChild(i)
+            if node <> invalid and node.uri <> invalid and node.uri = "pkg:/images/icons/favorite_selected.png"
+                m.favoriteMenu.getChild(i).blendColor = color
+            end if
+        end for
+    catch e
+        m.log.error("setHeartColor()", e.number, e.message)
+    end try
+end sub
+
+sub saveFavoriteItemSelected(msg)
+    data = msg.GetData()
+    m.selectedFavoriteItem = data
+    ' Favorite button
+    if m.selectedFavoriteItem <> invalid
+        if m.selectedFavoriteItem.favorite = true
+            m.favoriteMenu.iconUri = "pkg:/images/icons/favorite_selected.png"
+            m.favoriteMenu.focusedIconUri = "pkg:/images/icons/favorite_selected.png"
+            ' Make sure we set the Favorite Heart color for the appropriate child
+            setHeartColor("#cc3333")
+        else
+            m.favoriteMenu.iconUri = "pkg:/images/icons/favorite.png"
+            m.favoriteMenu.focusedIconUri = "pkg:/images/icons/favorite.png"
+            ' Make sure we set the Favorite Heart color for the appropriate child
+            setHeartColor("#cc3333")
+        end if
+    end if
+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)
+        m.menus[m.selectedItem].drawFocusFeedback = true
+
+        'If user presses down from button menu, focus first item.  If OK, focus checked item
+        if key = "down"
+            m.menus[m.selectedItem].jumpToItem = 0
+        else
+            m.menus[m.selectedItem].jumpToItem = m.menus[m.selectedItem].itemSelected
+        end if
+
+        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
+                ' Selected filter has options, move cursor to it
+                if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
+                    m.menus[m.selectedItem].setFocus(false)
+                    m.filterOptions.setFocus(true)
+                    return true
+                end if
+            end if
+        end if
+    else if key = "left"
+        if m.favoriteMenu.hasFocus()
+            m.favoriteMenu.setFocus(false)
+            m.menus[m.selectedItem].visible = true
+            m.buttons.setFocus(true)
+        end if
+
+        ' User wants to escape filter options
+        if m.filterOptions.isInFocusChain()
+            m.filterOptions.setFocus(false)
+            m.menus[m.selectedItem].setFocus(true)
+            return true
+        end if
+    else if key = "OK"
+
+        if m.menus[m.selectedItem].isInFocusChain()
+            ' Handle View Screen
+            if m.selectedItem = 0
+                m.selectedViewIndex = m.menus[0].itemSelected
+                m.top.view = m.viewNames[m.selectedViewIndex]
+            end if
+
+            ' Handle Sort screen
+            if m.selectedItem = 1
+                if m.menus[1].itemSelected <> m.selectedSortIndex
+                    m.menus[1].focusedCheckedIconUri = m.global.constants.icons.ascending_black
+                    m.menus[1].checkedIconUri = m.global.constants.icons.ascending_white
+
+                    m.selectedSortIndex = m.menus[1].itemSelected
+                    m.top.sortAscending = true
+                    m.top.sortField = m.sortNames[m.selectedSortIndex]
+                else
+
+                    if m.top.sortAscending = true
+                        m.top.sortAscending = false
+                        m.menus[1].focusedCheckedIconUri = m.global.constants.icons.descending_black
+                        m.menus[1].checkedIconUri = m.global.constants.icons.descending_white
+                    else
+                        m.top.sortAscending = true
+                        m.menus[1].focusedCheckedIconUri = m.global.constants.icons.ascending_black
+                        m.menus[1].checkedIconUri = m.global.constants.icons.ascending_white
+                    end if
+                end if
+            end if
+
+            ' 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
+                    m.selectedFilterIndex = m.menus[2].itemSelected
+                    m.top.filter = m.filterNames[m.selectedFilterIndex]
+                    m.top.filterOptions = {}
+                    return true
+                end if
+
+                ' Selected filter has options, move cursor to it
+                m.filterOptions.setFocus(true)
+                m.menus[m.selectedItem].setFocus(false)
+                return true
+            end if
+        end if
+
+        ' User pressed OK from inside the filter's options
+        if m.filterOptions.isInFocusChain()
+            selectedOptions = []
+            for i = 0 to m.filterOptions.checkedState.count() - 1
+                if m.filterOptions.checkedState[i]
+                    selectedValue = toString(m.filterOptions.content.getChild(i).title)
+                    selectedOptions.push(selectedValue)
+                end if
+            end for
+
+            if selectedOptions.Count() > 0
+                m.menus[2].checkedItem = m.menus[2].itemFocused
+                m.selectedFilterIndex = m.menus[2].itemFocused
+                m.top.filter = m.filterMenu.content.getChild(m.filterMenu.itemFocused).Name
+
+                newFilter = {}
+                newFilter[m.top.filter] = selectedOptions.join(m.filterMenu.content.getChild(m.filterMenu.itemFocused).delimiter)
+                m.top.filterOptions = newFilter
+            else
+                m.menus[2].checkedItem = 0
+                m.selectedFilterIndex = 0
+                m.top.filter = m.filterNames[0]
+                m.top.filterOptions = {}
+            end if
+
+            m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState = m.filterOptions.checkedState
+
+            return true
+        end if
+        return true
+    else if key = "back" or key = "up"
+        if key = "back" then hideChecklist()
+
+        m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
+        if m.menus[m.selectedItem].isInFocusChain()
+            m.buttons.setFocus(true)
+            m.menus[m.selectedItem].drawFocusFeedback = false
+            return true
+        end if
+    else if key = "options"
+        hideChecklist()
+        m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
+        m.menus[m.selectedItem].drawFocusFeedback = false
+        return false
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_LoadItemsTask2.bs.html b/docs/api/components_ItemGrid_LoadItemsTask2.bs.html new file mode 100644 index 000000000..c2d481835 --- /dev/null +++ b/docs/api/components_ItemGrid_LoadItemsTask2.bs.html @@ -0,0 +1,268 @@ +Source: components/ItemGrid/LoadItemsTask2.bs
On this page

components_ItemGrid_LoadItemsTask2.bs

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 = m.global.session.user.settings["itemgrid.Limit"]
+
+    if usersettingLimit <> invalid
+        m.top.limit = usersettingLimit
+    end if
+end sub
+
+sub loadItems()
+    results = []
+
+    sort_field = m.top.sortField
+
+    if m.top.sortAscending = true
+        sort_order = "Ascending"
+    else
+        sort_order = "Descending"
+    end if
+
+    if m.top.ItemType = "LogoImage"
+        logoImageExists = api.items.HeadImageURLByName(m.top.itemId, "logo")
+        if logoImageExists
+            m.top.content = [api.items.GetImageURL(m.top.itemId, "logo", 0, { "maxHeight": 500, "maxWidth": 500, "quality": "90" })]
+        else
+            m.top.content = []
+        end if
+
+        return
+    end if
+
+    params = {
+        limit: m.top.limit,
+        StartIndex: m.top.startIndex,
+        parentid: m.top.itemId,
+        SortBy: sort_field,
+        SortOrder: sort_order,
+        recursive: m.top.recursive,
+        Fields: "Overview",
+        StudioIds: m.top.studioIds,
+        genreIds: m.top.genreIds
+    }
+
+    ' Handle special case when getting names starting with numeral
+    if m.top.NameStartsWith <> ""
+        if m.top.NameStartsWith = "#"
+            if m.top.ItemType = "LiveTV" or m.top.ItemType = "TvChannel"
+                params.searchterm = "A"
+                params.append({ parentid: " " })
+            else
+                params.NameLessThan = "A"
+            end if
+        else
+            if m.top.ItemType = "LiveTV" or m.top.ItemType = "TvChannel"
+                params.searchterm = m.top.nameStartsWith
+                params.append({ parentid: " " })
+            else
+                params.NameStartsWith = m.top.nameStartsWith
+            end if
+        end if
+    end if
+
+    'reset data
+    if LCase(m.top.searchTerm) = LCase(tr("all"))
+        params.searchTerm = " "
+    else if m.top.searchTerm <> ""
+        params.searchTerm = m.top.searchTerm
+    end if
+
+    filter = LCase(m.top.filter)
+    if filter = "all"
+        ' do nothing
+    else if filter = "favorites"
+        params.append({ Filters: "IsFavorite" })
+        params.append({ isFavorite: true })
+    else if filter = "unplayed"
+        params.append({ Filters: "IsUnplayed" })
+    else if filter = "played"
+        params.append({ Filters: "IsPlayed" })
+    else if filter = "resumable"
+        params.append({ Filters: "IsResumable" })
+    end if
+
+    if isValid(m.top.filterOptions)
+        if m.top.filterOptions.count() > 0
+            params.append(m.top.filterOptions)
+        end if
+    end if
+
+    if m.top.ItemType <> ""
+        params.append({ IncludeItemTypes: m.top.ItemType })
+    end if
+
+    if m.top.ItemType = "LiveTV"
+        url = "LiveTv/Channels"
+        params.append({ UserId: m.global.session.user.id })
+    else if m.top.view = "Networks"
+        url = "Studios"
+        params.append({ UserId: m.global.session.user.id })
+    else if m.top.view = "Genres"
+        url = "Genres"
+        params.append({ UserId: m.global.session.user.id, includeItemTypes: m.top.itemType })
+    else if m.top.ItemType = "MusicArtist"
+        url = "Artists"
+        params.append({
+            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/", m.global.session.user.id)
+        params.append({ ImageTypeLimit: 1 })
+        params.append({ EnableImageTypes: "Primary,Backdrop,Banner,Thumb" })
+    else
+        url = Substitute("Users/{0}/Items/", m.global.session.user.id)
+    end if
+
+    resp = APIRequest(url, params)
+    data = getJson(resp)
+    if data <> invalid
+
+        if data.TotalRecordCount <> invalid then m.top.totalRecordCount = data.TotalRecordCount
+
+        for each item in data.Items
+            tmp = invalid
+            if item.Type = "Movie" or item.Type = "MusicVideo"
+                tmp = CreateObject("roSGNode", "MovieData")
+            else if item.Type = "Series"
+                tmp = CreateObject("roSGNode", "SeriesData")
+            else if item.Type = "BoxSet" or item.Type = "ManualPlaylistsFolder"
+                tmp = CreateObject("roSGNode", "CollectionData")
+            else if item.Type = "TvChannel"
+                tmp = CreateObject("roSGNode", "ChannelData")
+            else if item.Type = "Folder" or item.Type = "ChannelFolderItem" or item.Type = "CollectionFolder"
+                tmp = CreateObject("roSGNode", "FolderData")
+            else if item.Type = "Video" or item.Type = "Recording"
+                tmp = CreateObject("roSGNode", "VideoData")
+            else if item.Type = "Photo"
+                tmp = CreateObject("roSGNode", "PhotoData")
+            else if item.type = "PhotoAlbum"
+                tmp = CreateObject("roSGNode", "FolderData")
+            else if item.type = "Playlist"
+                tmp = CreateObject("roSGNode", "PlaylistData")
+                tmp.type = "Playlist"
+                tmp.image = PosterImage(item.id, { "maxHeight": 425, "maxWidth": 290, "quality": "90" })
+            else if item.type = "Episode"
+                tmp = CreateObject("roSGNode", "TVEpisode")
+            else if item.Type = "Genre"
+                tmp = CreateObject("roSGNode", "ContentNode")
+                tmp.title = item.name
+
+                genreData = api.users.GetItemsByQuery(m.global.session.user.id, {
+                    SortBy: "Random",
+                    SortOrder: "Ascending",
+                    IncludeItemTypes: m.top.itemType,
+                    Recursive: true,
+                    Fields: "PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo",
+                    ImageTypeLimit: 1,
+                    EnableImageTypes: "Primary",
+                    Limit: 6,
+                    GenreIds: item.id,
+                    EnableTotalRecordCount: false,
+                    ParentId: m.top.itemId
+                })
+
+                if genreData.Items.Count() > 5
+                    ' Add View All item to the start of the row
+                    row = tmp.createChild("FolderData")
+                    row.parentFolder = m.top.itemId
+                    row.title = tr("View All") + " " + item.name
+                    item.name = tr("View All") + " " + item.name
+                    row.json = item
+                    row.type = "Folder"
+
+                    if LCase(m.top.itemType) = "movie"
+                        genreItemImage = api.items.GetImageURL(item.id)
+                    else
+                        genreItemImage = invalid
+                        row.posterURL = invalid
+                    end if
+
+                    row.FHDPOSTERURL = genreItemImage
+                    row.HDPOSTERURL = genreItemImage
+                    row.SDPOSTERURL = genreItemImage
+                end if
+
+                for each genreItem in genreData.Items
+                    if LCase(m.top.itemType) = "movie"
+                        row = tmp.createChild("MovieData")
+                    else
+                        row = tmp.createChild("SeriesData")
+                    end if
+
+                    genreItemImage = api.items.GetImageURL(genreItem.id)
+                    row.title = genreItem.name
+                    row.FHDPOSTERURL = genreItemImage
+                    row.HDPOSTERURL = genreItemImage
+                    row.SDPOSTERURL = genreItemImage
+                    row.json = genreItem
+                    row.id = genreItem.id
+                    row.type = genreItem.type
+                end for
+
+            else if item.Type = "Studio"
+                tmp = CreateObject("roSGNode", "FolderData")
+            else if item.Type = "MusicAlbum"
+                tmp = CreateObject("roSGNode", "MusicAlbumData")
+                tmp.type = "MusicAlbum"
+                if api.items.HeadImageURLByName(item.id, "primary")
+                    tmp.posterURL = ImageURL(item.id, "Primary")
+                else
+                    tmp.posterURL = ImageURL(item.id, "backdrop")
+                end if
+            else if item.Type = "MusicArtist"
+                tmp = CreateObject("roSGNode", "MusicArtistData")
+            else if item.Type = "Audio"
+                tmp = CreateObject("roSGNode", "MusicSongData")
+                tmp.type = "Audio"
+                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.items.GetImageURL(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
+
+            else
+                m.log.warn("Unknown Type", item.Type)
+            end if
+
+            if tmp <> invalid
+                if item.Type <> "Genre" and item.Type <> "MusicGenre"
+                    tmp.parentFolder = m.top.itemId
+                    tmp.json = item
+                    if item.UserData <> invalid and item.UserData.isFavorite <> invalid
+                        tmp.favorite = item.UserData.isFavorite
+                    end if
+                end if
+
+                results.push(tmp)
+            end if
+        end for
+    end if
+    m.top.content = results
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_LoadVideoContentTask.bs.html b/docs/api/components_ItemGrid_LoadVideoContentTask.bs.html new file mode 100644 index 000000000..b70280880 --- /dev/null +++ b/docs/api/components_ItemGrid_LoadVideoContentTask.bs.html @@ -0,0 +1,912 @@ +Source: components/ItemGrid/LoadVideoContentTask.bs
On this page

components_ItemGrid_LoadVideoContentTask.bs

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"
+end sub
+
+sub loadItems()
+    ' 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 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, forceTranscoding)
+
+    if video.content = invalid
+        return invalid
+    end if
+
+    return video
+end function
+
+sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean)
+
+    meta = ItemMetaData(video.id)
+
+    if not isValid(meta)
+        video.errorMsg = "Error loading metadata"
+        video.content = invalid
+        return
+    end if
+
+    videotype = LCase(meta.type)
+
+    if videotype = "episode" or videotype = "series"
+        video.content.contenttype = "episode"
+    end if
+
+    video.chapters = meta.json.Chapters
+    video.content.title = meta.title
+    video.showID = meta.showID
+
+    user = AboutMe()
+    if user.Configuration.EnableNextEpisodeAutoPlay
+        if LCase(m.top.itemType) = "episode"
+            addNextEpisodesToQueue(video.showID)
+        end if
+    end if
+
+    playbackPosition = 0!
+
+    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 = ""
+
+    m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
+    video.videoId = video.id
+    video.mediaSourceId = mediaSourceId
+    video.audioIndex = audio_stream_idx
+
+    if not isValid(m.playbackInfo)
+        video.errorMsg = "Error loading playback info"
+        video.content = invalid
+        return
+    end if
+
+    video.PlaySessionId = m.playbackInfo.PlaySessionId
+
+    if meta.live
+        video.content.live = true
+        video.content.StreamFormat = "hls"
+    end if
+
+    video.container = getContainerType(meta)
+
+    if not isValid(m.playbackInfo.MediaSources[0])
+        m.playbackInfo = meta.json
+    end if
+
+    addSubtitlesToVideo(video, meta)
+
+    if meta.live
+        video.transcodeParams = {
+            "MediaSourceId": m.playbackInfo.MediaSources[0].Id,
+            "LiveStreamId": m.playbackInfo.MediaSources[0].LiveStreamId,
+            "PlaySessionId": video.PlaySessionId
+        }
+    end if
+
+
+    ' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
+    video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
+    fully_external = false
+
+
+    ' For h264/hevc video, Roku spec states that it supports specfic encoding levels
+    ' The device can decode content with a Higher Encoding level but may play it back with certain
+    ' artifacts. If the user preference is set, and the only reason the server says we need to
+    ' 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 = 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"
+                video.directPlaySupported = true
+                video.transcodeAvailable = true
+            end if
+        end if
+    end if
+
+    if video.directPlaySupported
+        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
+        ' Get transcoding reason
+        video.transcodeReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
+        video.content.url = buildURL(m.playbackInfo.MediaSources[0].TranscodingUrl)
+        video.isTranscoded = true
+    end if
+
+    setCertificateAuthority(video.content)
+    video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based
+
+    video.SelectedSubtitle = subtitle_idx
+
+    if not fully_external
+        video.content = authRequest(video.content)
+    end if
+end sub
+
+sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
+    protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
+    if protocol <> "file"
+        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
+            video.content.url = buildURL(uri[4])
+        else
+            fully_external = true
+            video.content.url = m.playbackInfo.MediaSources[0].Path
+        end if
+    else
+        params = {
+            "Static": "true",
+            "Container": video.container,
+            "PlaySessionId": video.PlaySessionId,
+            "AudioStreamIndex": audio_stream_idx
+        }
+
+        if mediaSourceId <> ""
+            params.MediaSourceId = mediaSourceId
+        end if
+
+        video.content.url = buildURL(Substitute("Videos/{0}/stream", video.id), params)
+    end if
+end sub
+
+sub addSubtitlesToVideo(video, meta)
+    subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
+    safesubs = subtitles["all"]
+    subtitleTracks = []
+
+    if m.global.session.user.settings["playback.subs.onlytext"] = true
+        safesubs = subtitles["text"]
+    end if
+
+    for each subtitle in safesubs
+        subtitleTracks.push(subtitle.track)
+    end for
+
+    video.content.SubtitleTracks = subtitleTracks
+    video.fullSubtitleData = safesubs
+end sub
+
+
+'
+' Extract array of Transcode Reasons from the content URL
+' @returns Array of Strings
+function getTranscodeReasons(url as string) as object
+
+    regex = CreateObject("roRegex", "&TranscodeReasons=([^&]*)", "")
+    match = regex.Match(url)
+
+    if match.count() > 1
+        return match[1].Split(",")
+    end if
+
+    return []
+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
+        return false
+    end if
+
+    if meta.json.MediaStreams[0] = invalid
+        return false
+    end if
+
+    streamInfo = { Codec: meta.json.MediaStreams[0].codec }
+    if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
+        streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
+    end if
+    if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
+        'CanDecodeVideo() requires the .container to be format: โ€œmp4โ€, โ€œhlsโ€, โ€œmkvโ€, โ€œismโ€, โ€œdashโ€, โ€œtsโ€ if its to direct stream
+        if meta.json.MediaSources[0].container = "mov"
+            streamInfo.Container = "mp4"
+        else
+            streamInfo.Container = meta.json.MediaSources[0].container
+        end if
+    end if
+
+    decodeResult = devinfo.CanDecodeVideo(streamInfo)
+    return decodeResult <> invalid and decodeResult.result
+
+end function
+
+function getContainerType(meta as object) as string
+    ' Determine the file type of the video file source
+    if meta.json.mediaSources = invalid then return ""
+
+    container = meta.json.mediaSources[0].container
+    if container = invalid
+        container = ""
+    else if container = "m4v" or container = "mov"
+        container = "mp4"
+    end if
+
+    return container
+end function
+
+' Add next episodes to the playback queue
+sub addNextEpisodesToQueue(showID)
+    ' Don't queue next episodes if we already have a playback queue
+    maxQueueCount = 1
+
+    if m.top.isIntro
+        maxQueueCount = 2
+    end if
+
+    if m.global.queueManager.callFunc("getCount") > maxQueueCount then return
+
+    videoID = m.top.itemId
+
+    ' 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
+        end if
+    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
+
+'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)
+    tracks = { "forced": [], "default": [], "normal": [], "text": [] }
+    'Too many args for using substitute
+    prefered_lang = m.global.session.user.configuration.SubtitleLanguagePreference
+    for each stream in MediaStreams
+        if stream.type = "Subtitle"
+
+            url = ""
+            if isValid(stream.DeliveryUrl)
+                url = buildURL(stream.DeliveryUrl)
+            end if
+
+            stream = {
+                "Track": { "Language": stream.language, "Description": stream.displaytitle, "TrackName": url },
+                "IsTextSubtitleStream": stream.IsTextSubtitleStream,
+                "Index": stream.index,
+                "IsDefault": stream.IsDefault,
+                "IsForced": stream.IsForced,
+                "IsExternal": stream.IsExternal,
+                "IsEncoded": stream.DeliveryMethod = "Encode"
+            }
+            if stream.isForced
+                trackType = "forced"
+            else if stream.IsDefault
+                trackType = "default"
+            else if stream.IsTextSubtitleStream
+                trackType = "text"
+            else
+                trackType = "normal"
+            end if
+            if prefered_lang <> "" and prefered_lang = stream.Track.Language
+                tracks[trackType].unshift(stream)
+            else
+                tracks[trackType].push(stream)
+            end if
+        end if
+    end for
+
+    tracks["default"].append(tracks["normal"])
+    tracks["forced"].append(tracks["default"])
+    tracks["forced"].append(tracks["text"])
+
+    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()
+    return {
+        "aar": "Afar",
+        "abk": "Abkhazian",
+        "ace": "Achinese",
+        "ach": "Acoli",
+        "ada": "Adangme",
+        "ady": "Adyghe; Adygei",
+        "afa": "Afro-Asiatic languages",
+        "afh": "Afrihili",
+        "afr": "Afrikaans",
+        "ain": "Ainu",
+        "aka": "Akan",
+        "akk": "Akkadian",
+        "alb": "Albanian",
+        "ale": "Aleut",
+        "alg": "Algonquian languages",
+        "alt": "Southern Altai",
+        "amh": "Amharic",
+        "ang": "English, Old (ca.450-1100)",
+        "anp": "Angika",
+        "apa": "Apache languages",
+        "ara": "Arabic",
+        "arc": "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)",
+        "arg": "Aragonese",
+        "arm": "Armenian",
+        "arn": "Mapudungun; Mapuche",
+        "arp": "Arapaho",
+        "art": "Artificial languages",
+        "arw": "Arawak",
+        "asm": "Assamese",
+        "ast": "Asturian; Bable; Leonese; Asturleonese",
+        "ath": "Athapascan languages",
+        "aus": "Australian languages",
+        "ava": "Avaric",
+        "ave": "Avestan",
+        "awa": "Awadhi",
+        "aym": "Aymara",
+        "aze": "Azerbaijani",
+        "bad": "Banda languages",
+        "bai": "Bamileke languages",
+        "bak": "Bashkir",
+        "bal": "Baluchi",
+        "bam": "Bambara",
+        "ban": "Balinese",
+        "baq": "Basque",
+        "bas": "Basa",
+        "bat": "Baltic languages",
+        "bej": "Beja; Bedawiyet",
+        "bel": "Belarusian",
+        "bem": "Bemba",
+        "ben": "Bengali",
+        "ber": "Berber languages",
+        "bho": "Bhojpuri",
+        "bih": "Bihari languages",
+        "bik": "Bikol",
+        "bin": "Bini; Edo",
+        "bis": "Bislama",
+        "bla": "Siksika",
+        "bnt": "Bantu (Other)",
+        "bos": "Bosnian",
+        "bra": "Braj",
+        "bre": "Breton",
+        "btk": "Batak languages",
+        "bua": "Buriat",
+        "bug": "Buginese",
+        "bul": "Bulgarian",
+        "bur": "Burmese",
+        "byn": "Blin; Bilin",
+        "cad": "Caddo",
+        "cai": "Central American Indian languages",
+        "car": "Galibi Carib",
+        "cat": "Catalan; Valencian",
+        "cau": "Caucasian languages",
+        "ceb": "Cebuano",
+        "cel": "Celtic languages",
+        "cha": "Chamorro",
+        "chb": "Chibcha",
+        "che": "Chechen",
+        "chg": "Chagatai",
+        "chi": "Chinese",
+        "chk": "Chuukese",
+        "chm": "Mari",
+        "chn": "Chinook jargon",
+        "cho": "Choctaw",
+        "chp": "Chipewyan; Dene Suline",
+        "chr": "Cherokee",
+        "chu": "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic",
+        "chv": "Chuvash",
+        "chy": "Cheyenne",
+        "cmc": "Chamic languages",
+        "cop": "Coptic",
+        "cor": "Cornish",
+        "cos": "Corsican",
+        "cpe": "Creoles and pidgins, English based",
+        "cpf": "Creoles and pidgins, French-based ",
+        "cpp": "Creoles and pidgins, Portuguese-based ",
+        "cre": "Cree",
+        "crh": "Crimean Tatar; Crimean Turkish",
+        "crp": "Creoles and pidgins ",
+        "csb": "Kashubian",
+        "cus": "Cushitic languages",
+        "cze": "Czech",
+        "dak": "Dakota",
+        "dan": "Danish",
+        "dar": "Dargwa",
+        "day": "Land Dayak languages",
+        "del": "Delaware",
+        "den": "Slave (Athapascan)",
+        "dgr": "Dogrib",
+        "din": "Dinka",
+        "div": "Divehi; Dhivehi; Maldivian",
+        "doi": "Dogri",
+        "dra": "Dravidian languages",
+        "dsb": "Lower Sorbian",
+        "dua": "Duala",
+        "dum": "Dutch, Middle (ca.1050-1350)",
+        "dut": "Dutch; Flemish",
+        "dyu": "Dyula",
+        "dzo": "Dzongkha",
+        "efi": "Efik",
+        "egy": "Egyptian (Ancient)",
+        "eka": "Ekajuk",
+        "elx": "Elamite",
+        "eng": "English",
+        "enm": "English, Middle (1100-1500)",
+        "epo": "Esperanto",
+        "est": "Estonian",
+        "ewe": "Ewe",
+        "ewo": "Ewondo",
+        "fan": "Fang",
+        "fao": "Faroese",
+        "fat": "Fanti",
+        "fij": "Fijian",
+        "fil": "Filipino; Pilipino",
+        "fin": "Finnish",
+        "fiu": "Finno-Ugrian languages",
+        "fon": "Fon",
+        "fre": "French",
+        "frm": "French, Middle (ca.1400-1600)",
+        "fro": "French, Old (842-ca.1400)",
+        "frc": "French (Canada)",
+        "frr": "Northern Frisian",
+        "frs": "Eastern Frisian",
+        "fry": "Western Frisian",
+        "ful": "Fulah",
+        "fur": "Friulian",
+        "gaa": "Ga",
+        "gay": "Gayo",
+        "gba": "Gbaya",
+        "gem": "Germanic languages",
+        "geo": "Georgian",
+        "ger": "German",
+        "gez": "Geez",
+        "gil": "Gilbertese",
+        "gla": "Gaelic; Scottish Gaelic",
+        "gle": "Irish",
+        "glg": "Galician",
+        "glv": "Manx",
+        "gmh": "German, Middle High (ca.1050-1500)",
+        "goh": "German, Old High (ca.750-1050)",
+        "gon": "Gondi",
+        "gor": "Gorontalo",
+        "got": "Gothic",
+        "grb": "Grebo",
+        "grc": "Greek, Ancient (to 1453)",
+        "gre": "Greek, Modern (1453-)",
+        "grn": "Guarani",
+        "gsw": "Swiss German; Alemannic; Alsatian",
+        "guj": "Gujarati",
+        "gwi": "Gwich'in",
+        "hai": "Haida",
+        "hat": "Haitian; Haitian Creole",
+        "hau": "Hausa",
+        "haw": "Hawaiian",
+        "heb": "Hebrew",
+        "her": "Herero",
+        "hil": "Hiligaynon",
+        "him": "Himachali languages; Western Pahari languages",
+        "hin": "Hindi",
+        "hit": "Hittite",
+        "hmn": "Hmong; Mong",
+        "hmo": "Hiri Motu",
+        "hrv": "Croatian",
+        "hsb": "Upper Sorbian",
+        "hun": "Hungarian",
+        "hup": "Hupa",
+        "iba": "Iban",
+        "ibo": "Igbo",
+        "ice": "Icelandic",
+        "ido": "Ido",
+        "iii": "Sichuan Yi; Nuosu",
+        "ijo": "Ijo languages",
+        "iku": "Inuktitut",
+        "ile": "Interlingue; Occidental",
+        "ilo": "Iloko",
+        "ina": "Interlingua (International Auxiliary Language Association)",
+        "inc": "Indic languages",
+        "ind": "Indonesian",
+        "ine": "Indo-European languages",
+        "inh": "Ingush",
+        "ipk": "Inupiaq",
+        "ira": "Iranian languages",
+        "iro": "Iroquoian languages",
+        "ita": "Italian",
+        "jav": "Javanese",
+        "jbo": "Lojban",
+        "jpn": "Japanese",
+        "jpr": "Judeo-Persian",
+        "jrb": "Judeo-Arabic",
+        "kaa": "Kara-Kalpak",
+        "kab": "Kabyle",
+        "kac": "Kachin; Jingpho",
+        "kal": "Kalaallisut; Greenlandic",
+        "kam": "Kamba",
+        "kan": "Kannada",
+        "kar": "Karen languages",
+        "kas": "Kashmiri",
+        "kau": "Kanuri",
+        "kaw": "Kawi",
+        "kaz": "Kazakh",
+        "kbd": "Kabardian",
+        "kha": "Khasi",
+        "khi": "Khoisan languages",
+        "khm": "Central Khmer",
+        "kho": "Khotanese; Sakan",
+        "kik": "Kikuyu; Gikuyu",
+        "kin": "Kinyarwanda",
+        "kir": "Kirghiz; Kyrgyz",
+        "kmb": "Kimbundu",
+        "kok": "Konkani",
+        "kom": "Komi",
+        "kon": "Kongo",
+        "kor": "Korean",
+        "kos": "Kosraean",
+        "kpe": "Kpelle",
+        "krc": "Karachay-Balkar",
+        "krl": "Karelian",
+        "kro": "Kru languages",
+        "kru": "Kurukh",
+        "kua": "Kuanyama; Kwanyama",
+        "kum": "Kumyk",
+        "kur": "Kurdish",
+        "kut": "Kutenai",
+        "lad": "Ladino",
+        "lah": "Lahnda",
+        "lam": "Lamba",
+        "lao": "Lao",
+        "lat": "Latin",
+        "lav": "Latvian",
+        "lez": "Lezghian",
+        "lim": "Limburgan; Limburger; Limburgish",
+        "lin": "Lingala",
+        "lit": "Lithuanian",
+        "lol": "Mongo",
+        "loz": "Lozi",
+        "ltz": "Luxembourgish; Letzeburgesch",
+        "lua": "Luba-Lulua",
+        "lub": "Luba-Katanga",
+        "lug": "Ganda",
+        "lui": "Luiseno",
+        "lun": "Lunda",
+        "luo": "Luo (Kenya and Tanzania)",
+        "lus": "Lushai",
+        "mac": "Macedonian",
+        "mad": "Madurese",
+        "mag": "Magahi",
+        "mah": "Marshallese",
+        "mai": "Maithili",
+        "mak": "Makasar",
+        "mal": "Malayalam",
+        "man": "Mandingo",
+        "mao": "Maori",
+        "map": "Austronesian languages",
+        "mar": "Marathi",
+        "mas": "Masai",
+        "may": "Malay",
+        "mdf": "Moksha",
+        "mdr": "Mandar",
+        "men": "Mende",
+        "mga": "Irish, Middle (900-1200)",
+        "mic": "Mi'kmaq; Micmac",
+        "min": "Minangkabau",
+        "mis": "Uncoded languages",
+        "mkh": "Mon-Khmer languages",
+        "mlg": "Malagasy",
+        "mlt": "Maltese",
+        "mnc": "Manchu",
+        "mni": "Manipuri",
+        "mno": "Manobo languages",
+        "moh": "Mohawk",
+        "mon": "Mongolian",
+        "mos": "Mossi",
+        "mul": "Multiple languages",
+        "mun": "Munda languages",
+        "mus": "Creek",
+        "mwl": "Mirandese",
+        "mwr": "Marwari",
+        "myn": "Mayan languages",
+        "myv": "Erzya",
+        "nah": "Nahuatl languages",
+        "nai": "North American Indian languages",
+        "nap": "Neapolitan",
+        "nau": "Nauru",
+        "nav": "Navajo; Navaho",
+        "nbl": "Ndebele, South; South Ndebele",
+        "nde": "Ndebele, North; North Ndebele",
+        "ndo": "Ndonga",
+        "nds": "Low German; Low Saxon; German, Low; Saxon, Low",
+        "nep": "Nepali",
+        "new": "Nepal Bhasa; Newari",
+        "nia": "Nias",
+        "nic": "Niger-Kordofanian languages",
+        "niu": "Niuean",
+        "nno": "Norwegian Nynorsk; Nynorsk, Norwegian",
+        "nob": "Bokmรฅl, Norwegian; Norwegian Bokmรฅl",
+        "nog": "Nogai",
+        "non": "Norse, Old",
+        "nor": "Norwegian",
+        "nqo": "N'Ko",
+        "nso": "Pedi; Sepedi; Northern Sotho",
+        "nub": "Nubian languages",
+        "nwc": "Classical Newari; Old Newari; Classical Nepal Bhasa",
+        "nya": "Chichewa; Chewa; Nyanja",
+        "nym": "Nyamwezi",
+        "nyn": "Nyankole",
+        "nyo": "Nyoro",
+        "nzi": "Nzima",
+        "oci": "Occitan (post 1500); Provenรงal",
+        "oji": "Ojibwa",
+        "ori": "Oriya",
+        "orm": "Oromo",
+        "osa": "Osage",
+        "oss": "Ossetian; Ossetic",
+        "ota": "Turkish, Ottoman (1500-1928)",
+        "oto": "Otomian languages",
+        "paa": "Papuan languages",
+        "pag": "Pangasinan",
+        "pal": "Pahlavi",
+        "pam": "Pampanga; Kapampangan",
+        "pan": "Panjabi; Punjabi",
+        "pap": "Papiamento",
+        "pau": "Palauan",
+        "peo": "Persian, Old (ca.600-400 B.C.)",
+        "per": "Persian",
+        "phi": "Philippine languages",
+        "phn": "Phoenician",
+        "pli": "Pali",
+        "pol": "Polish",
+        "pon": "Pohnpeian",
+        "por": "Portuguese",
+        "pob": "Portuguese (Brazil)",
+        "pra": "Prakrit languages",
+        "pro": "Provenรงal, Old (to 1500)",
+        "pus": "Pushto; Pashto",
+        "qaa-qtz": "Reserved for local use",
+        "que": "Quechua",
+        "raj": "Rajasthani",
+        "rap": "Rapanui",
+        "rar": "Rarotongan; Cook Islands Maori",
+        "roa": "Romance languages",
+        "roh": "Romansh",
+        "rom": "Romany",
+        "rum": "Romanian; Moldavian; Moldovan",
+        "run": "Rundi",
+        "rup": "Aromanian; Arumanian; Macedo-Romanian",
+        "rus": "Russian",
+        "sad": "Sandawe",
+        "sag": "Sango",
+        "sah": "Yakut",
+        "sai": "South American Indian (Other)",
+        "sal": "Salishan languages",
+        "sam": "Samaritan Aramaic",
+        "san": "Sanskrit",
+        "sas": "Sasak",
+        "sat": "Santali",
+        "scn": "Sicilian",
+        "sco": "Scots",
+        "sel": "Selkup",
+        "sem": "Semitic languages",
+        "sga": "Irish, Old (to 900)",
+        "sgn": "Sign Languages",
+        "shn": "Shan",
+        "sid": "Sidamo",
+        "sin": "Sinhala; Sinhalese",
+        "sio": "Siouan languages",
+        "sit": "Sino-Tibetan languages",
+        "sla": "Slavic languages",
+        "slo": "Slovak",
+        "slv": "Slovenian",
+        "sma": "Southern Sami",
+        "sme": "Northern Sami",
+        "smi": "Sami languages",
+        "smj": "Lule Sami",
+        "smn": "Inari Sami",
+        "smo": "Samoan",
+        "sms": "Skolt Sami",
+        "sna": "Shona",
+        "snd": "Sindhi",
+        "snk": "Soninke",
+        "sog": "Sogdian",
+        "som": "Somali",
+        "son": "Songhai languages",
+        "sot": "Sotho, Southern",
+        "spa": "Spanish; Latin",
+        "spa": "Spanish; Castilian",
+        "srd": "Sardinian",
+        "srn": "Sranan Tongo",
+        "srp": "Serbian",
+        "srr": "Serer",
+        "ssa": "Nilo-Saharan languages",
+        "ssw": "Swati",
+        "suk": "Sukuma",
+        "sun": "Sundanese",
+        "sus": "Susu",
+        "sux": "Sumerian",
+        "swa": "Swahili",
+        "swe": "Swedish",
+        "syc": "Classical Syriac",
+        "syr": "Syriac",
+        "tah": "Tahitian",
+        "tai": "Tai languages",
+        "tam": "Tamil",
+        "tat": "Tatar",
+        "tel": "Telugu",
+        "tem": "Timne",
+        "ter": "Tereno",
+        "tet": "Tetum",
+        "tgk": "Tajik",
+        "tgl": "Tagalog",
+        "tha": "Thai",
+        "tib": "Tibetan",
+        "tig": "Tigre",
+        "tir": "Tigrinya",
+        "tiv": "Tiv",
+        "tkl": "Tokelau",
+        "tlh": "Klingon; tlhIngan-Hol",
+        "tli": "Tlingit",
+        "tmh": "Tamashek",
+        "tog": "Tonga (Nyasa)",
+        "ton": "Tonga (Tonga Islands)",
+        "tpi": "Tok Pisin",
+        "tsi": "Tsimshian",
+        "tsn": "Tswana",
+        "tso": "Tsonga",
+        "tuk": "Turkmen",
+        "tum": "Tumbuka",
+        "tup": "Tupi languages",
+        "tur": "Turkish",
+        "tut": "Altaic languages",
+        "tvl": "Tuvalu",
+        "twi": "Twi",
+        "tyv": "Tuvinian",
+        "udm": "Udmurt",
+        "uga": "Ugaritic",
+        "uig": "Uighur; Uyghur",
+        "ukr": "Ukrainian",
+        "umb": "Umbundu",
+        "und": "Undetermined",
+        "urd": "Urdu",
+        "uzb": "Uzbek",
+        "vai": "Vai",
+        "ven": "Venda",
+        "vie": "Vietnamese",
+        "vol": "Volapรผk",
+        "vot": "Votic",
+        "wak": "Wakashan languages",
+        "wal": "Walamo",
+        "war": "Waray",
+        "was": "Washo",
+        "wel": "Welsh",
+        "wen": "Sorbian languages",
+        "wln": "Walloon",
+        "wol": "Wolof",
+        "xal": "Kalmyk; Oirat",
+        "xho": "Xhosa",
+        "yao": "Yao",
+        "yap": "Yapese",
+        "yid": "Yiddish",
+        "yor": "Yoruba",
+        "ypk": "Yupik languages",
+        "zap": "Zapotec",
+        "zbl": "Blissymbols; Blissymbolics; Bliss",
+        "zen": "Zenaga",
+        "zgh": "Standard Moroccan Tamazight",
+        "zha": "Zhuang; Chuang",
+        "znd": "Zande languages",
+        "zul": "Zulu",
+        "zun": "Zuni",
+        "zxx": "No linguistic content; Not applicable",
+        "zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
+    }
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_MovieLibraryView.bs.html b/docs/api/components_ItemGrid_MovieLibraryView.bs.html new file mode 100644 index 000000000..41a50e6b0 --- /dev/null +++ b/docs/api/components_ItemGrid_MovieLibraryView.bs.html @@ -0,0 +1,939 @@ +Source: components/ItemGrid/MovieLibraryView.bs
On this page

components_ItemGrid_MovieLibraryView.bs

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")
+    m.voiceBox = m.top.findNode("voiceBox")
+    m.backdrop = m.top.findNode("backdrop")
+    m.newBackdrop = m.top.findNode("backdropTransition")
+    m.emptyText = m.top.findNode("emptyText")
+    m.selectedMovieName = m.top.findNode("selectedMovieName")
+    m.selectedMovieOverview = m.top.findNode("selectedMovieOverview")
+    m.selectedMovieProductionYear = m.top.findNode("selectedMovieProductionYear")
+    m.selectedMovieOfficialRating = m.top.findNode("selectedMovieOfficialRating")
+    m.movieLogo = m.top.findNode("movieLogo")
+    m.swapAnimation = m.top.findNode("backroundSwapAnimation")
+    m.Alpha = m.top.findNode("AlphaMenu")
+    m.AlphaSelected = m.top.findNode("AlphaSelected")
+    m.micButton = m.top.findNode("micButton")
+    m.micButtonText = m.top.findNode("micButtonText")
+    m.communityRatingGroup = m.top.findNode("communityRatingGroup")
+    m.criticRatingIcon = m.top.findNode("criticRatingIcon")
+    m.criticRatingGroup = m.top.findNode("criticRatingGroup")
+    m.overhang = m.top.getScene().findNode("overhang")
+    m.genreList = m.top.findNode("genrelist")
+    m.infoGroup = m.top.findNode("infoGroup")
+    m.star = m.top.findNode("star")
+end sub
+
+sub init()
+    setupNodes()
+
+    m.overhang.isVisible = false
+
+    m.showItemCount = m.global.session.user.settings["itemgrid.showItemCount"]
+
+    m.swapAnimation.observeField("state", "swapDone")
+
+    m.loadedRows = 0
+    m.loadedItems = 0
+
+    m.data = CreateObject("roSGNode", "ContentNode")
+
+    m.itemGrid.content = m.data
+
+    m.genreData = CreateObject("roSGNode", "ContentNode")
+    m.genreList.observeField("itemSelected", "onGenreItemSelected")
+    m.genreList.content = m.genreData
+
+    m.itemGrid.observeField("itemFocused", "onItemFocused")
+    m.itemGrid.observeField("itemSelected", "onItemSelected")
+    m.itemGrid.observeField("alphaSelected", "onItemalphaSelected")
+
+    'Voice filter setup
+    m.voiceBox.voiceEnabled = true
+    m.voiceBox.active = true
+    m.voiceBox.observeField("text", "onvoiceFilter")
+    'set voice help text
+    m.voiceBox.hintText = tr("Use voice remote to search")
+
+    'backdrop
+    m.newBackdrop.observeField("loadStatus", "newBGLoaded")
+
+    'Background Image Queued for loading
+    m.queuedBGUri = ""
+
+    'Item sort - maybe load defaults from user prefs?
+    m.sortField = "SortName"
+    m.sortAscending = true
+
+    m.filter = "All"
+    m.filterOptions = {}
+    m.favorite = "Favorite"
+
+    m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
+    m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
+    m.getFiltersTask = createObject("roSGNode", "GetFiltersTask")
+
+    'set inital counts for overhang before content is loaded.
+    m.loadItemsTask.totalRecordCount = 0
+
+    'Get reset folder setting
+    m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
+
+    'Hide voice search if device does not have voice remote
+    if m.global.device.hasVoiceRemote = false
+        m.micButton.visible = false
+        m.micButtonText.visible = false
+    end if
+end sub
+
+sub OnScreenHidden()
+    if not m.overhang.isVisible
+        m.overhang.disableMoveAnimation = true
+        m.overhang.isVisible = true
+        m.overhang.disableMoveAnimation = false
+    end if
+end sub
+
+sub OnScreenShown()
+    m.overhang.isVisible = false
+
+    if m.top.lastFocus <> invalid
+        m.top.lastFocus.setFocus(true)
+    else
+        m.top.setFocus(true)
+    end if
+end sub
+
+'
+'Load initial set of Data
+sub loadInitialItems()
+    m.loadItemsTask.control = "stop"
+    startLoadingSpinner(false)
+
+    if m.top.parentItem.json.Type = "CollectionFolder"
+        m.top.HomeLibraryItem = m.top.parentItem.Id
+    end if
+
+    if m.top.parentItem.backdropUrl <> invalid
+        SetBackground(m.top.parentItem.backdropUrl)
+    else
+        SetBackground("")
+    end if
+
+    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 = 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 m.top.parentItem.json.type = "Studio"
+        m.loadItemsTask.studioIds = m.top.parentItem.id
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+        m.loadItemsTask.genreIds = ""
+    else if m.top.parentItem.json.type = "Genre"
+        m.loadItemsTask.genreIds = m.top.parentItem.id
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+        m.loadItemsTask.studioIds = ""
+    else if m.view = "Movies" or m.options.view = "Movies"
+        m.loadItemsTask.studioIds = ""
+        m.loadItemsTask.genreIds = ""
+    else
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    end if
+
+    m.loadItemsTask.nameStartsWith = m.top.alphaSelected
+    m.loadItemsTask.searchTerm = m.voiceBox.text
+    m.emptyText.visible = false
+    m.loadItemsTask.sortField = m.sortField
+    m.loadItemsTask.sortAscending = m.sortAscending
+    m.loadItemsTask.filter = m.filter
+    m.loadItemsTask.filterOptions = m.filterOptions
+    m.loadItemsTask.startIndex = 0
+
+    ' Load Item Types
+    if getCollectionType() = "movies"
+        m.loadItemsTask.itemType = "Movie"
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    end if
+
+    ' By default we load movies
+    m.loadItemsTask.studioIds = ""
+    m.loadItemsTask.view = "Movies"
+    m.itemGrid.translation = "[96, 650]"
+    m.itemGrid.itemSize = "[230, 310]"
+    m.itemGrid.rowHeights = "[310]"
+    m.itemGrid.numRows = "2"
+    m.selectedMovieOverview.visible = true
+    m.infoGroup.visible = true
+    m.top.showItemTitles = "hidealways"
+
+    if m.options.view = "Studios" or m.view = "Studios"
+        m.itemGrid.translation = "[96, 60]"
+        m.itemGrid.numRows = "3"
+        m.loadItemsTask.view = "Networks"
+        m.top.imageDisplayMode = "scaleToFit"
+        m.selectedMovieOverview.visible = false
+        m.infoGroup.visible = false
+    else if LCase(m.options.view) = "moviesgrid" or LCase(m.view) = "moviesgrid"
+        m.itemGrid.translation = "[96, 60]"
+        m.itemGrid.numRows = "3"
+        m.selectedMovieOverview.visible = false
+        m.infoGroup.visible = false
+        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]"
+        else
+            m.itemGrid.itemSize = "[230, 350]"
+            m.itemGrid.rowHeights = "[350]"
+        end if
+    else if m.options.view = "Genres" or m.view = "Genres"
+        m.loadItemsTask.StudioIds = m.top.parentItem.Id
+        m.loadItemsTask.view = "Genres"
+        m.movieLogo.visible = false
+        m.selectedMovieName.visible = false
+        m.selectedMovieOverview.visible = false
+        m.infoGroup.visible = false
+    end if
+
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    m.loadItemsTask.control = "RUN"
+
+    m.getFiltersTask.observeField("filters", "FilterDataLoaded")
+    m.getFiltersTask.params = {
+        userid: m.global.session.user.id,
+        parentid: m.top.parentItem.Id,
+        includeitemtypes: "Movie"
+    }
+    m.getFiltersTask.control = "RUN"
+end sub
+
+' Set Movies view, sort, and filter options
+sub setMoviesOptions(options)
+
+    options.views = [
+        { "Title": tr("Movies (Presentation)"), "Name": "Movies" },
+        { "Title": tr("Movies (Grid)"), "Name": "MoviesGrid" },
+        { "Title": tr("Studios"), "Name": "Studios" },
+        { "Title": tr("Genres"), "Name": "Genres" }
+    ]
+
+    if m.top.parentItem.json.type = "Genre"
+        options.views = [
+            { "Title": tr("Movies (Presentation)"), "Name": "Movies" },
+            { "Title": tr("Movies (Grid)"), "Name": "MoviesGrid" },
+        ]
+    end if
+
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
+        { "Title": tr("CRITIC_RATING"), "Name": "CriticRating" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
+        { "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+        { "Title": tr("RUNTIME"), "Name": "Runtime" }
+    ]
+
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" },
+        { "Title": tr("Played"), "Name": "Played" },
+        { "Title": tr("Unplayed"), "Name": "Unplayed" },
+        { "Title": tr("Resumable"), "Name": "Resumable" }
+    ]
+
+    if m.options.view = "Genres" or m.view = "Genres"
+        options.sort = [{ "Title": tr("TITLE"), "Name": "SortName" }]
+        options.filter = []
+    end if
+
+    if m.options.view = "Studios" or m.view = "Studios"
+        options.sort = [
+            { "Title": tr("TITLE"), "Name": "SortName" },
+            { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        ]
+        options.filter = [
+            { "Title": tr("All"), "Name": "All" },
+            { "Title": tr("Favorites"), "Name": "Favorites" }
+        ]
+    end if
+end sub
+
+' Return parent collection type
+function getCollectionType() as string
+    if m.top.parentItem.collectionType = invalid
+        return m.top.parentItem.Type
+    else
+        return m.top.parentItem.CollectionType
+    end if
+end function
+
+' Search string array for search value. Return if it's found
+function inStringArray(array, searchValue) as boolean
+    for each item in array
+        if lcase(item) = lcase(searchValue) then return true
+    end for
+    return false
+end function
+
+' Data to display when options button selected
+sub setSelectedOptions(options)
+
+    ' Set selected view option
+    for each o in options.views
+        if o.Name = m.view
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.view = o.Name
+        end if
+    end for
+
+    ' Set selected sort option
+    for each o in options.sort
+        if o.Name = m.sortField
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.sortField = o.Name
+        end if
+    end for
+
+    ' Set selected filter
+    for each o in options.filter
+        if o.Name = m.filter
+            o.Selected = true
+            m.options.filter = o.Name
+        end if
+
+        ' Select selected filter options
+        if isValid(o.options) and isValid(m.filterOptions)
+            if o.options.Count() > 0 and m.filterOptions.Count() > 0
+                if LCase(o.Name) = LCase(m.filterOptions.keys()[0])
+                    selectedFilterOptions = m.filterOptions[m.filterOptions.keys()[0]].split(o.delimiter)
+                    checkedState = []
+
+                    for each availableFilterOption in o.options
+                        matchFound = false
+
+                        for each selectedFilterOption in selectedFilterOptions
+                            if LCase(toString(availableFilterOption).trim()) = LCase(selectedFilterOption.trim())
+                                matchFound = true
+                            end if
+                        end for
+
+                        checkedState.push(matchFound)
+                    end for
+
+                    o.checkedState = checkedState
+                end if
+            end if
+        end if
+    end for
+
+    m.options.options = options
+end sub
+
+'
+' Logo Image Loaded Event Handler
+sub FilterDataLoaded(msg)
+    options = {}
+    options.filter = []
+    options.favorite = []
+
+    setMoviesOptions(options)
+
+    data = msg.GetData()
+    m.getFiltersTask.unobserveField("filters")
+
+    if not isValid(data) then return
+
+    ' Add Movie filters from the API data
+    if LCase(m.loadItemsTask.view) = "movies"
+        if isValid(data.genres)
+            options.filter.push({ "Title": tr("Genres"), "Name": "Genres", "Options": data.genres, "Delimiter": "|", "CheckedState": [] })
+        end if
+
+        if isValid(data.OfficialRatings)
+            options.filter.push({ "Title": tr("Parental Ratings"), "Name": "OfficialRatings", "Options": data.OfficialRatings, "Delimiter": "|", "CheckedState": [] })
+        end if
+
+        if isValid(data.Years)
+            options.filter.push({ "Title": tr("Years"), "Name": "Years", "Options": data.Years, "Delimiter": ",", "CheckedState": [] })
+        end if
+    end if
+
+    setSelectedOptions(options)
+
+    m.options.options = options
+end sub
+
+
+'
+' Logo Image Loaded Event Handler
+sub LogoImageLoaded(msg)
+    data = msg.GetData()
+    m.loadLogoTask.unobserveField("content")
+    m.loadLogoTask.content = []
+
+    if data.Count() > 0
+        m.movieLogo.uri = data[0]
+        m.movieLogo.visible = true
+    else
+        m.selectedMovieName.visible = true
+    end if
+end sub
+
+'
+'Handle loaded data, and add to Grid
+sub ItemDataLoaded(msg)
+    m.top.alphaActive = false
+    itemData = msg.GetData()
+    m.loadItemsTask.unobserveField("content")
+    m.loadItemsTask.content = []
+
+    if itemData = invalid
+        m.Loading = false
+        return
+    end if
+
+    if m.loadItemsTask.view = "Genres"
+        ' Reset genre list data
+        m.genreData.removeChildren(m.genreData.getChildren(-1, 0))
+
+        for each item in itemData
+            m.genreData.appendChild(item)
+        end for
+
+        m.itemGrid.opacity = "0"
+        m.genreList.opacity = "1"
+
+        m.itemGrid.setFocus(false)
+        m.genreList.setFocus(true)
+
+        m.loading = false
+        stopLoadingSpinner()
+        ' Return focus to options menu if it was opened while library was loading
+        if m.options.visible
+            m.options.setFocus(true)
+        end if
+        return
+    end if
+
+    m.itemGrid.opacity = "1"
+    m.genreList.opacity = "0"
+
+    m.itemGrid.setFocus(true)
+    m.genreList.setFocus(false)
+
+    if m.data.getChildCount() = 0
+        m.itemGrid.jumpToItem = 0
+    end if
+
+    for each item in itemData
+        m.data.appendChild(item)
+    end for
+
+    'Update the stored counts
+    m.loadedItems = m.itemGrid.content.getChildCount()
+    m.loadedRows = m.loadedItems / m.itemGrid.numColumns
+    m.Loading = false
+    'If there are no items to display, show message
+    if m.loadedItems = 0
+        m.selectedMovieOverview.visible = false
+        m.infoGroup.visible = false
+
+        m.movieLogo.visible = false
+        m.movieLogo.uri = ""
+
+        m.selectedMovieName.visible = false
+
+        SetName("")
+        SetOverview("")
+        SetOfficialRating("")
+        SetProductionYear("")
+        setFieldText("runtime", "")
+        setFieldText("communityRating", "")
+        setFieldText("criticRatingLabel", "")
+        m.criticRatingIcon.uri = ""
+        m.star.uri = ""
+
+        m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
+        m.emptyText.visible = true
+    end if
+
+    stopLoadingSpinner()
+    ' Return focus to options menu if it was opened while library was loading
+    if m.options.visible
+        m.options.setFocus(true)
+    end if
+end sub
+
+'
+'Set Selected Movie Name
+sub SetName(movieName as string)
+    m.selectedMovieName.text = movieName
+end sub
+
+'
+'Set Selected Movie Overview
+sub SetOverview(movieOverview as string)
+    m.selectedMovieOverview.text = movieOverview
+end sub
+
+'
+'Set Selected Movie OfficialRating
+sub SetOfficialRating(movieOfficialRating as string)
+    m.selectedMovieOfficialRating.text = movieOfficialRating
+end sub
+
+'
+'Set Selected Movie ProductionYear
+sub SetProductionYear(movieProductionYear)
+    m.selectedMovieProductionYear.text = movieProductionYear
+end sub
+
+'
+'Set Background Image
+sub SetBackground(backgroundUri as string)
+    if backgroundUri = ""
+        m.backdrop.opacity = 0
+    end if
+
+    'If a new image is being loaded, or transitioned to, store URL to load next
+    if m.swapAnimation.state <> "stopped" or m.newBackdrop.loadStatus = "loading"
+        m.queuedBGUri = backgroundUri
+        return
+    end if
+
+    m.newBackdrop.uri = backgroundUri
+end sub
+
+'
+'Handle new item being focused
+sub onItemFocused()
+    focusedRow = m.itemGrid.currFocusRow
+
+    itemInt = m.itemGrid.itemFocused
+
+    ' If no selected item, set background to parent backdrop
+    if itemInt = -1
+        return
+    end if
+
+    m.movieLogo.visible = false
+    m.selectedMovieName.visible = false
+
+    ' Load more data if focus is within last 5 rows, and there are more items to load
+    if focusedRow >= m.loadedRows - 5 and m.loadeditems < m.loadItemsTask.totalRecordCount
+        loadMoreData()
+    end if
+
+    m.selectedFavoriteItem = getItemFocused()
+    m.communityRatingGroup.visible = false
+    m.criticRatingGroup.visible = false
+
+    if not isValid(m.selectedFavoriteItem)
+        return
+    end if
+
+    if LCase(m.options.view) = "studios" or LCase(m.view) = "studios"
+        return
+    else if LCase(m.options.view) = "moviesgrid" or LCase(m.view) = "moviesgrid"
+        return
+    end if
+
+    itemData = m.selectedFavoriteItem.json
+
+    m.star.uri = "pkg:/images/sharp_star_white_18dp.png"
+
+    if isValid(itemData.communityRating)
+        setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
+        m.communityRatingGroup.visible = true
+    end if
+
+    if isValid(itemData.CriticRating)
+        setFieldText("criticRatingLabel", itemData.criticRating)
+
+        tomato = "pkg:/images/rotten.png"
+
+        if itemData.CriticRating > 60
+            tomato = "pkg:/images/fresh.png"
+        end if
+
+        m.criticRatingIcon.uri = tomato
+        m.criticRatingGroup.visible = true
+    end if
+
+    if isValid(itemData.Name)
+        SetName(itemData.Name)
+    else
+        SetName("")
+    end if
+
+    if isValid(itemData.Overview)
+        SetOverview(itemData.Overview)
+    else
+        SetOverview("")
+    end if
+
+    if isValid(itemData.ProductionYear)
+        SetProductionYear(str(itemData.ProductionYear))
+    else
+        SetProductionYear("")
+    end if
+
+    if type(itemData.RunTimeTicks) = "LongInteger"
+        setFieldText("runtime", stri(getRuntime(itemData.RunTimeTicks)) + " mins")
+    else
+        setFieldText("runtime", "")
+    end if
+
+    if isValid(itemData.OfficialRating)
+        SetOfficialRating(itemData.OfficialRating)
+    else
+        SetOfficialRating("")
+    end if
+
+    m.loadLogoTask.itemId = itemData.id
+    m.loadLogoTask.itemType = "LogoImage"
+    m.loadLogoTask.observeField("content", "LogoImageLoaded")
+    m.loadLogoTask.control = "RUN"
+
+    ' Set Background to item backdrop
+    SetBackground(m.selectedFavoriteItem.backdropUrl)
+end sub
+
+function getRuntime(runTimeTicks) as integer
+    return round(runTimeTicks / 600000000.0)
+end function
+
+function round(f as float) as integer
+    ' BrightScript only has a "floor" round
+    ' This compares floor to floor + 1 to find which is closer
+    m = int(f)
+    n = m + 1
+    x = abs(f - m)
+    y = abs(f - n)
+    if y > x
+        return m
+    else
+        return n
+    end if
+end function
+
+sub setFieldText(field, value)
+    node = m.top.findNode(field)
+    if node = invalid or value = invalid then return
+
+    ' Handle non strings... Which _shouldn't_ happen, but hey
+    if type(value) = "roInt" or type(value) = "Integer"
+        value = str(value)
+    else if type(value) = "roFloat" or type(value) = "Float"
+        value = str(value)
+    else if type(value) <> "roString" and type(value) <> "String"
+        value = ""
+    end if
+
+    node.text = value
+end sub
+
+'
+'When Image Loading Status changes
+sub newBGLoaded()
+    'If image load was sucessful, start the fade swap
+    if m.newBackdrop.loadStatus = "ready"
+        m.swapAnimation.control = "start"
+    end if
+end sub
+
+'
+'Swap Complete
+sub swapDone()
+    if m.swapAnimation.state = "stopped"
+        'Set main BG node image and hide transitioning node
+        m.backdrop.uri = m.newBackdrop.uri
+        m.backdrop.opacity = 1
+        m.newBackdrop.opacity = 0
+
+        'If there is another one to load
+        if m.newBackdrop.uri <> m.queuedBGUri and m.queuedBGUri <> ""
+            SetBackground(m.queuedBGUri)
+            m.queuedBGUri = ""
+        end if
+    end if
+end sub
+
+'
+'Load next set of items
+sub loadMoreData()
+    if m.Loading = true then return
+
+    startLoadingSpinner(false)
+    m.Loading = true
+    m.loadItemsTask.startIndex = m.loadedItems
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    m.loadItemsTask.control = "RUN"
+end sub
+
+'
+'Item Selected
+sub onItemSelected()
+    m.top.selectedItem = m.itemGrid.content.getChild(m.itemGrid.itemSelected)
+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])
+    end if
+    return invalid
+end function
+
+'
+'Genre Item Selected
+sub onGenreItemSelected()
+    m.top.selectedItem = m.genreList.content.getChild(m.genreList.rowItemSelected[0]).getChild(m.genreList.rowItemSelected[1])
+end sub
+
+sub onItemalphaSelected()
+    if m.top.alphaSelected <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+
+        m.genreData = CreateObject("roSGNode", "ContentNode")
+        m.genreList.content = m.genreData
+
+        m.loadItemsTask.searchTerm = ""
+        m.VoiceBox.text = ""
+        m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
+        loadInitialItems()
+    end if
+end sub
+
+sub onvoiceFilter()
+    if m.VoiceBox.text <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        m.top.alphaSelected = ""
+        m.loadItemsTask.NameStartsWith = " "
+        m.loadItemsTask.searchTerm = m.voiceBox.text
+        m.loadItemsTask.recursive = true
+        loadInitialItems()
+    end if
+end sub
+
+
+'
+'Check if options updated and any reloading required
+sub optionsClosed()
+    reload = false
+
+    if m.options.sortField <> m.sortField or m.options.sortAscending <> m.sortAscending
+        m.sortField = m.options.sortField
+        m.sortAscending = m.options.sortAscending
+        reload = true
+
+        sortAscendingStr = "true"
+
+        'Store sort settings
+        if not m.sortAscending
+            sortAscendingStr = "false"
+        end if
+
+        set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
+        set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", sortAscendingStr)
+    end if
+
+    if m.options.filter <> m.filter
+        m.filter = m.options.filter
+        reload = true
+        set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
+    end if
+
+    if not isValid(m.options.filterOptions)
+        m.filterOptions = {}
+    end if
+
+    if not AssocArrayEqual(m.options.filterOptions, m.filterOptions)
+        m.filterOptions = m.options.filterOptions
+        reload = true
+        set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.options.filterOptions))
+    end if
+
+    m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
+
+    if m.options.view <> m.view
+        m.view = m.options.view
+        set_user_setting("display." + m.top.parentItem.Id + ".landing", m.view)
+
+        ' Reset any filtering or search terms
+        m.top.alphaSelected = ""
+        m.loadItemsTask.NameStartsWith = " "
+        m.loadItemsTask.searchTerm = ""
+        m.filter = "All"
+        m.filterOptions = {}
+        m.sortField = "SortName"
+        m.sortAscending = true
+
+        ' Reset view to defaults
+        set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
+        set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", "true")
+        set_user_setting("display." + m.top.parentItem.Id + ".filter", m.filter)
+        set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.filterOptions))
+
+        reload = true
+    end if
+
+    if reload
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        loadInitialItems()
+    end if
+
+    m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+    m.genreList.setFocus(m.genreList.opacity = 1)
+end sub
+
+sub onChannelSelected(msg)
+    node = msg.getRoSGNode()
+    m.top.lastFocus = lastFocusedChild(node)
+    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)
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "left" and m.voiceBox.isinFocusChain()
+        m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+        m.genreList.setFocus(m.genreList.opacity = 1)
+        m.voiceBox.setFocus(false)
+    end if
+
+    if key = "options"
+        if m.options.visible = true
+            m.options.visible = false
+            m.top.removeChild(m.options)
+            optionsClosed()
+        else
+
+            itemSelected = m.selectedFavoriteItem
+            if itemSelected <> invalid
+                m.options.selectedFavoriteItem = itemSelected
+            end if
+
+            m.options.visible = true
+            m.top.appendChild(m.options)
+            m.options.setFocus(true)
+        end if
+        return true
+    else if key = "back"
+        if m.options.visible = true
+            m.options.visible = false
+            optionsClosed()
+            return true
+        else
+            m.global.sceneManager.callfunc("popScene")
+            m.loadItemsTask.control = "stop"
+            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"
+        if m.itemGrid.isinFocusChain()
+            m.top.alphaActive = true
+            m.itemGrid.setFocus(false)
+            alpha = m.alpha.getChild(0).findNode("Alphamenu")
+            alpha.setFocus(true)
+            return true
+        else if m.genreList.isinFocusChain()
+            m.top.alphaActive = true
+            m.genreList.setFocus(false)
+            alpha = m.alpha.getChild(0).findNode("Alphamenu")
+            alpha.setFocus(true)
+            return true
+        end if
+
+    else if key = "right" and m.Alpha.isinFocusChain()
+        m.top.alphaActive = false
+        m.Alpha.setFocus(false)
+        m.Alpha.visible = true
+
+        m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+        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
+        return true
+    else if key = "replay" and m.genreList.isinFocusChain()
+        if m.resetGrid = true
+            m.genreList.animateToItem = 0
+        else
+            m.genreList.jumpToItem = 0
+        end if
+        return true
+    end if
+
+    if key = "replay"
+        m.loadItemsTask.searchTerm = ""
+        m.loadItemsTask.nameStartsWith = ""
+        m.voiceBox.text = ""
+        m.top.alphaSelected = ""
+        m.loadItemsTask.filter = "All"
+        m.filter = "All"
+        m.filterOptions = {}
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        loadInitialItems()
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_MusicArtistGridItem.bs.html b/docs/api/components_ItemGrid_MusicArtistGridItem.bs.html new file mode 100644 index 000000000..104b5751c --- /dev/null +++ b/docs/api/components_ItemGrid_MusicArtistGridItem.bs.html @@ -0,0 +1,86 @@ +Source: components/ItemGrid/MusicArtistGridItem.bs
On this page

components_ItemGrid_MusicArtistGridItem.bs

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")
+    m.posterText = m.top.findNode("posterText")
+    m.posterText.font.size = 30
+    m.backdrop = m.top.findNode("backdrop")
+
+    m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
+
+    'Parent is MarkupGrid and it's parent is the ItemGrid
+    m.topParent = m.top.GetParent().GetParent()
+
+    'Get the imageDisplayMode for these grid items
+    if m.topParent.imageDisplayMode <> invalid
+        m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
+    end if
+
+    m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
+    m.posterText.visible = false
+    m.postTextBackground.visible = false
+
+end sub
+
+sub itemContentChanged()
+    m.backdrop.blendColor = "#101010"
+
+    m.posterText.visible = false
+    m.postTextBackground.visible = false
+
+    if isValid(m.topParent.showItemTitles)
+        if LCase(m.topParent.showItemTitles) = "showalways"
+            m.posterText.visible = true
+            m.postTextBackground.visible = true
+        end if
+    end if
+
+    itemData = m.top.itemContent
+
+    if not isValid(itemData) then return
+
+    if LCase(itemData.type) = "musicalbum"
+        m.backdrop.uri = "pkg:/images/icons/album.png"
+    else if LCase(itemData.type) = "musicartist"
+        m.backdrop.uri = "pkg:/images/missingArtist.png"
+    else if LCase(itemData.json.type) = "musicgenre"
+        m.backdrop.uri = "pkg:/images/icons/musicFolder.png"
+    end if
+
+    m.itemPoster.uri = itemData.PosterUrl
+    m.posterText.text = itemData.title
+
+    'If Poster not loaded, ensure "blue box" is shown until loaded
+    if m.itemPoster.loadStatus <> "ready"
+        m.backdrop.visible = true
+    end if
+    if m.top.itemHasFocus then focusChanged()
+end sub
+
+'Display or hide title Visibility on focus change
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        m.posterText.repeatCount = -1
+    else
+        m.posterText.repeatCount = 0
+    end if
+
+    if isValid(m.topParent.showItemTitles)
+        if LCase(m.topParent.showItemTitles) = "showonhover"
+            m.posterText.visible = m.top.itemHasFocus
+            m.postTextBackground.visible = m.posterText.visible
+        end if
+    end if
+end sub
+
+'Hide backdrop and text when poster loaded
+sub onPosterLoadStatusChanged()
+    if m.itemPoster.loadStatus = "ready"
+        m.backdrop.visible = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ItemGrid_MusicLibraryView.bs.html b/docs/api/components_ItemGrid_MusicLibraryView.bs.html new file mode 100644 index 000000000..ac3235d24 --- /dev/null +++ b/docs/api/components_ItemGrid_MusicLibraryView.bs.html @@ -0,0 +1,799 @@ +Source: components/ItemGrid/MusicLibraryView.bs
On this page

components_ItemGrid_MusicLibraryView.bs

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")
+    m.voiceBox = m.top.findNode("voiceBox")
+    m.backdrop = m.top.findNode("backdrop")
+    m.newBackdrop = m.top.findNode("backdropTransition")
+    m.emptyText = m.top.findNode("emptyText")
+    m.selectedArtistName = m.top.findNode("selectedArtistName")
+    m.selectedArtistSongCount = m.top.findNode("selectedArtistSongCount")
+    m.selectedArtistAlbumCount = m.top.findNode("selectedArtistAlbumCount")
+    m.selectedArtistGenres = m.top.findNode("selectedArtistGenres")
+    m.artistLogo = m.top.findNode("artistLogo")
+    m.swapAnimation = m.top.findNode("backroundSwapAnimation")
+    m.Alpha = m.top.findNode("AlphaMenu")
+    m.AlphaSelected = m.top.findNode("AlphaSelected")
+    m.micButton = m.top.findNode("micButton")
+    m.micButtonText = m.top.findNode("micButtonText")
+    m.overhang = m.top.getScene().findNode("overhang")
+    m.genreList = m.top.findNode("genrelist")
+end sub
+
+sub init()
+    setupNodes()
+
+    m.overhang.isVisible = false
+
+    m.showItemCount = m.global.session.user.settings["itemgrid.showItemCount"]
+
+    m.swapAnimation.observeField("state", "swapDone")
+
+    m.loadedRows = 0
+    m.loadedItems = 0
+
+    m.data = CreateObject("roSGNode", "ContentNode")
+
+    m.itemGrid.content = m.data
+
+    m.genreData = CreateObject("roSGNode", "ContentNode")
+    m.genreList.observeField("itemSelected", "onGenreItemSelected")
+    m.genreList.observeField("itemFocused", "onGenreItemFocused")
+    m.genreList.content = m.genreData
+
+    m.itemGrid.observeField("itemFocused", "onItemFocused")
+    m.itemGrid.observeField("itemSelected", "onItemSelected")
+    m.itemGrid.observeField("alphaSelected", "onItemalphaSelected")
+
+    'Voice filter setup
+    m.voiceBox.voiceEnabled = true
+    m.voiceBox.active = true
+    m.voiceBox.observeField("text", "onvoiceFilter")
+    'set voice help text
+    m.voiceBox.hintText = tr("Use voice remote to search")
+
+    'backdrop
+    m.newBackdrop.observeField("loadStatus", "newBGLoaded")
+
+    'Background Image Queued for loading
+    m.queuedBGUri = ""
+
+    'Item sort - maybe load defaults from user prefs?
+    m.sortField = "SortName"
+    m.sortAscending = true
+
+    m.filter = "All"
+    m.favorite = "Favorite"
+
+    m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
+    m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
+
+    'set inital counts for overhang before content is loaded.
+    m.loadItemsTask.totalRecordCount = 0
+
+    'Get reset folder setting
+    m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
+
+    'Hide voice search if device does not have voice remote
+    if m.global.device.hasVoiceRemote = false
+        m.micButton.visible = false
+        m.micButtonText.visible = false
+    end if
+end sub
+
+sub OnScreenHidden()
+    if not m.overhang.isVisible
+        m.overhang.disableMoveAnimation = true
+        m.overhang.isVisible = true
+        m.overhang.disableMoveAnimation = false
+    end if
+end sub
+
+sub OnScreenShown()
+    m.overhang.isVisible = false
+
+    if m.top.lastFocus <> invalid
+        m.top.lastFocus.setFocus(true)
+    else
+        m.top.setFocus(true)
+    end if
+end sub
+
+'
+'Load initial set of Data
+sub loadInitialItems()
+    m.loadItemsTask.control = "stop"
+    startLoadingSpinner(false)
+
+    if LCase(m.top.parentItem.json.Type) = "collectionfolder"
+        m.top.HomeLibraryItem = m.top.parentItem.Id
+    end if
+
+    if m.top.parentItem.backdropUrl <> invalid
+        SetBackground(m.top.parentItem.backdropUrl)
+    else
+        SetBackground("")
+    end if
+
+    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
+
+    m.top.showItemTitles = m.global.session.user.settings["itemgrid.gridTitles"]
+
+    if LCase(m.top.parentItem.json.type) = "musicgenre"
+        m.itemGrid.translation = "[96, 60]"
+        m.loadItemsTask.itemType = "MusicAlbum"
+        m.loadItemsTask.recursive = true
+        m.loadItemsTask.genreIds = m.top.parentItem.id
+        m.loadItemsTask.itemId = m.top.parentItem.parentFolder
+    else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation"
+        m.loadItemsTask.genreIds = ""
+        m.top.showItemTitles = "hidealways"
+    else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid"
+        m.loadItemsTask.genreIds = ""
+    else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid"
+        m.loadItemsTask.genreIds = ""
+    else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation"
+        m.loadItemsTask.genreIds = ""
+        m.top.showItemTitles = "hidealways"
+    else
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    end if
+
+    m.loadItemsTask.nameStartsWith = m.top.alphaSelected
+    m.loadItemsTask.searchTerm = m.voiceBox.text
+    m.emptyText.visible = false
+    m.loadItemsTask.sortField = m.sortField
+    m.loadItemsTask.sortAscending = m.sortAscending
+    m.loadItemsTask.filter = m.filter
+    m.loadItemsTask.startIndex = 0
+
+    ' Load Item Types
+    if getCollectionType() = "music"
+        m.loadItemsTask.itemType = "MusicArtist"
+        m.loadItemsTask.itemId = m.top.parentItem.Id
+    end if
+
+    ' By default we load Artists
+    m.loadItemsTask.view = "Artists"
+    m.itemGrid.translation = "[96, 420]"
+    m.itemGrid.numRows = "3"
+
+    if LCase(m.options.view) = "albums" or LCase(m.view) = "albums"
+        m.itemGrid.translation = "[96, 60]"
+        m.itemGrid.numRows = "4"
+        m.loadItemsTask.itemType = "MusicAlbum"
+        m.top.imageDisplayMode = "scaleToFit"
+    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
+        m.loadItemsTask.view = "Genres"
+        m.artistLogo.visible = false
+        m.selectedArtistName.visible = false
+    end if
+
+    if LCase(m.top.parentItem.json.type) = "musicgenre"
+        m.itemGrid.translation = "[96, 60]"
+        m.itemGrid.numRows = "4"
+        m.artistLogo.visible = false
+        m.selectedArtistName.visible = false
+    end if
+
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    m.loadItemsTask.control = "RUN"
+    SetUpOptions()
+end sub
+
+' Set Music view, sort, and filter options
+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" }
+    ]
+
+    if LCase(m.top.parentItem.json.type) = "musicgenre"
+        options.views = [
+            { "Title": tr("Albums"), "Name": "Albums" }
+        ]
+    end if
+
+    options.sort = [
+        { "Title": tr("TITLE"), "Name": "SortName" },
+        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
+        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
+    ]
+
+    options.filter = [
+        { "Title": tr("All"), "Name": "All" },
+        { "Title": tr("Favorites"), "Name": "Favorites" }
+    ]
+
+    if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
+        options.sort = [
+            { "Title": tr("TITLE"), "Name": "SortName" },
+        ]
+        options.filter = []
+    end if
+
+    if LCase(m.options.view) = "albums" or LCase(m.view) = "albums"
+        options.sort = [
+            { "Title": tr("TITLE"), "Name": "SortName" },
+            { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
+        ]
+    end if
+end sub
+
+' Return parent collection type
+function getCollectionType() as string
+    if m.top.parentItem.collectionType = invalid
+        return LCase(m.top.parentItem.Type)
+    else
+        return LCase(m.top.parentItem.CollectionType)
+    end if
+end function
+
+' Search string array for search value. Return if it's found
+function inStringArray(array, searchValue) as boolean
+    for each item in array
+        if lcase(item) = lcase(searchValue) then return true
+    end for
+    return false
+end function
+
+' Data to display when options button selected
+sub SetUpOptions()
+    options = {}
+    options.filter = []
+    options.favorite = []
+
+    setMusicOptions(options)
+
+    ' Set selected view option
+    for each o in options.views
+        if LCase(o.Name) = LCase(m.view)
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.view = o.Name
+        end if
+    end for
+
+    ' Set selected sort option
+    for each o in options.sort
+        if LCase(o.Name) = LCase(m.sortField)
+            o.Selected = true
+            o.Ascending = m.sortAscending
+            m.options.sortField = o.Name
+        end if
+    end for
+
+    ' Set selected filter option
+    for each o in options.filter
+        if LCase(o.Name) = LCase(m.filter)
+            o.Selected = true
+            m.options.filter = o.Name
+        end if
+    end for
+
+    m.options.options = options
+end sub
+
+'
+' Logo Image Loaded Event Handler
+sub LogoImageLoaded(msg)
+    data = msg.GetData()
+    m.loadLogoTask.unobserveField("content")
+    m.loadLogoTask.content = []
+
+    if data.Count() > 0
+        m.artistLogo.uri = data[0]
+        m.artistLogo.visible = true
+    else
+        m.selectedArtistName.visible = true
+    end if
+end sub
+
+'
+'Handle loaded data, and add to Grid
+sub ItemDataLoaded(msg)
+    stopLoadingSpinner()
+    m.top.alphaActive = false
+    itemData = msg.GetData()
+    m.loadItemsTask.unobserveField("content")
+    m.loadItemsTask.content = []
+
+    if itemData = invalid
+        m.Loading = false
+        return
+    end if
+
+    if LCase(m.loadItemsTask.view) = "genres"
+        for each item in itemData
+            m.genreData.appendChild(item)
+        end for
+
+        m.itemGrid.opacity = "0"
+        m.genreList.opacity = "1"
+
+        m.itemGrid.setFocus(false)
+        m.genreList.setFocus(true)
+
+        m.loadedItems = m.genreList.content.getChildCount()
+        m.loadedRows = m.loadedItems / m.genreList.numColumns
+
+        m.loading = false
+        return
+    end if
+
+    m.itemGrid.opacity = "1"
+    m.genreList.opacity = "0"
+
+    m.itemGrid.setFocus(true)
+    m.genreList.setFocus(false)
+
+    for each item in itemData
+        m.data.appendChild(item)
+    end for
+
+    'Update the stored counts
+    m.loadedItems = m.itemGrid.content.getChildCount()
+    m.loadedRows = m.loadedItems / m.itemGrid.numColumns
+    m.Loading = false
+    'If there are no items to display, show message
+    if m.loadedItems = 0
+        m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
+        m.emptyText.visible = true
+    end if
+end sub
+
+'
+'Set Selected Artist Name
+sub SetName(artistName as string)
+    m.selectedArtistName.text = artistName
+end sub
+
+'
+'Set Selected Artist Song Count
+sub SetSongCount(totalCount)
+    appendText = " " + tr("Songs")
+    if totalCount = 1
+        appendText = " " + tr("Song")
+    end if
+
+    m.selectedArtistSongCount.text = totalCount.tostr() + appendText
+end sub
+'
+'Set Selected Artist Album Count
+sub SetAlbumCount(totalCount)
+    appendText = " " + tr("Albums")
+    if totalCount = 1
+        appendText = " " + tr("Album")
+    end if
+
+    m.selectedArtistAlbumCount.text = totalCount.tostr() + appendText
+end sub
+
+'
+'Set Selected Artist Genres
+sub SetGenres(artistGenres)
+    m.selectedArtistGenres.text = artistGenres.join(", ")
+end sub
+
+'
+'Set Background Image
+sub SetBackground(backgroundUri as string)
+    if backgroundUri = ""
+        m.backdrop.opacity = 0
+    end if
+
+    'If a new image is being loaded, or transitioned to, store URL to load next
+    if LCase(m.swapAnimation.state) <> "stopped" or LCase(m.newBackdrop.loadStatus) = "loading"
+        m.queuedBGUri = backgroundUri
+        return
+    end if
+
+    m.newBackdrop.uri = backgroundUri
+end sub
+
+'
+'Handle new item being focused
+sub onItemFocused()
+    focusedRow = m.itemGrid.currFocusRow
+
+    itemInt = m.itemGrid.itemFocused
+
+    ' If no selected item, set background to parent backdrop
+    if itemInt = -1
+        return
+    end if
+
+    m.artistLogo.visible = false
+    m.selectedArtistName.visible = false
+    m.selectedArtistGenres.visible = false
+    m.selectedArtistSongCount.visible = false
+    m.selectedArtistAlbumCount.visible = false
+
+    ' Load more data if focus is within last 5 rows, and there are more items to load
+    if focusedRow >= m.loadedRows - 5 and m.loadeditems < m.loadItemsTask.totalRecordCount
+        loadMoreData()
+    end if
+
+    m.selectedFavoriteItem = getItemFocused()
+
+    if LCase(m.options.view) = "albums" or LCase(m.view) = "albums" or LCase(m.top.parentItem.json.type) = "musicgenre"
+        return
+    end if
+
+    if LCase(m.options.view) = "artistsgrid" or LCase(m.view) = "artistsgrid"
+        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
+
+    if not m.selectedArtistSongCount.visible
+        m.selectedArtistSongCount.visible = true
+    end if
+
+    if not m.selectedArtistAlbumCount.visible
+        m.selectedArtistAlbumCount.visible = true
+    end if
+
+    itemData = m.selectedFavoriteItem.json
+
+    if isValid(itemData.SongCount)
+        SetSongCount(itemData.SongCount)
+    else
+        SetSongCount("")
+    end if
+
+    if isValid(itemData.AlbumCount)
+        SetAlbumCount(itemData.AlbumCount)
+    else
+        SetAlbumCount("")
+    end if
+
+    if isValid(itemData.Genres)
+        SetGenres(itemData.Genres)
+    else
+        SetGenres([])
+    end if
+
+    if isValid(itemData.Name)
+        SetName(itemData.Name)
+    else
+        SetName("")
+    end if
+
+    m.loadLogoTask.itemId = itemData.id
+    m.loadLogoTask.itemType = "LogoImage"
+    m.loadLogoTask.observeField("content", "LogoImageLoaded")
+    m.loadLogoTask.control = "RUN"
+
+    ' Set Background to item backdrop
+    SetBackground(m.selectedFavoriteItem.backdropUrl)
+end sub
+
+sub setFieldText(field, value)
+    node = m.top.findNode(field)
+    if node = invalid or value = invalid then return
+
+    ' Handle non strings... Which _shouldn't_ happen, but hey
+    if type(value) = "roInt" or type(value) = "Integer"
+        value = str(value)
+    else if type(value) = "roFloat" or type(value) = "Float"
+        value = str(value)
+    else if type(value) <> "roString" and type(value) <> "String"
+        value = ""
+    end if
+
+    node.text = value
+end sub
+
+'
+'When Image Loading Status changes
+sub newBGLoaded()
+    'If image load was sucessful, start the fade swap
+    if LCase(m.newBackdrop.loadStatus) = "ready"
+        m.swapAnimation.control = "start"
+    end if
+end sub
+
+'
+'Swap Complete
+sub swapDone()
+    if LCase(m.swapAnimation.state) = "stopped"
+        'Set main BG node image and hide transitioning node
+        m.backdrop.uri = m.newBackdrop.uri
+        m.backdrop.opacity = 1
+        m.newBackdrop.opacity = 0
+
+        'If there is another one to load
+        if m.newBackdrop.uri <> m.queuedBGUri and m.queuedBGUri <> ""
+            SetBackground(m.queuedBGUri)
+            m.queuedBGUri = ""
+        end if
+    end if
+end sub
+
+'
+'Load next set of items
+sub loadMoreData()
+    if m.Loading = true then return
+
+    startLoadingSpinner(false)
+    m.Loading = true
+    m.loadItemsTask.startIndex = m.loadedItems
+    m.loadItemsTask.observeField("content", "ItemDataLoaded")
+    m.loadItemsTask.control = "RUN"
+end sub
+
+'
+'Item Selected
+sub onItemSelected()
+    m.top.selectedItem = m.itemGrid.content.getChild(m.itemGrid.itemSelected)
+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.itemFocused)
+        return m.genreList.content.getChild(m.genreList.itemFocused)
+    end if
+    return invalid
+end function
+
+'
+'Genre Item Selected
+sub onGenreItemSelected()
+    m.top.selectedItem = m.genreList.content.getChild(m.genreList.itemSelected)
+end sub
+
+'
+'Genre Item Focused
+sub onGenreItemFocused()
+    focusedRow = m.genreList.currFocusRow
+
+    ' Load more data if focus is within last 5 rows, and there are more items to load
+    if focusedRow >= m.loadedRows - 5 and m.loadeditems < m.loadItemsTask.totalRecordCount
+        loadMoreData()
+    end if
+end sub
+
+sub onItemalphaSelected()
+    if m.top.alphaSelected <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+
+        m.genreData = CreateObject("roSGNode", "ContentNode")
+        m.genreList.content = m.genreData
+
+        m.loadItemsTask.searchTerm = ""
+        m.VoiceBox.text = ""
+        m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
+        loadInitialItems()
+    end if
+end sub
+
+sub onvoiceFilter()
+    if m.VoiceBox.text <> ""
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        m.top.alphaSelected = ""
+        m.loadItemsTask.NameStartsWith = " "
+        m.loadItemsTask.searchTerm = m.voiceBox.text
+        m.loadItemsTask.recursive = true
+        loadInitialItems()
+    end if
+end sub
+
+
+'
+'Check if options updated and any reloading required
+sub optionsClosed()
+    reload = false
+
+    if m.options.sortField <> m.sortField or m.options.sortAscending <> m.sortAscending
+        m.sortField = m.options.sortField
+        m.sortAscending = m.options.sortAscending
+        reload = true
+
+        sortAscendingStr = "true"
+
+        'Store sort settings
+        if not m.sortAscending
+            sortAscendingStr = "false"
+        end if
+
+        set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
+        set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", sortAscendingStr)
+    end if
+
+    if m.options.filter <> m.filter
+        m.filter = m.options.filter
+        reload = true
+        set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
+    end if
+
+    m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
+
+    if m.options.view <> m.view
+        m.view = m.options.view
+        m.top.view = m.view
+        set_user_setting("display." + m.top.parentItem.Id + ".landing", m.view)
+
+        ' Reset any filtering or search terms
+        m.top.alphaSelected = ""
+        m.loadItemsTask.NameStartsWith = " "
+        m.loadItemsTask.searchTerm = ""
+        m.filter = "All"
+        m.sortField = "SortName"
+        m.sortAscending = true
+
+        ' Reset view to defaults
+        set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
+        set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", "true")
+        set_user_setting("display." + m.top.parentItem.Id + ".filter", m.filter)
+
+        reload = true
+    end if
+
+    if reload
+        m.loadedRows = 0
+        m.loadedItems = 0
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.genreData = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        m.genreList.content = m.genreData
+        loadInitialItems()
+    end if
+
+    m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+    m.genreList.setFocus(m.genreList.opacity = 1)
+end sub
+
+sub onChannelSelected(msg)
+    node = msg.getRoSGNode()
+    m.top.lastFocus = lastFocusedChild(node)
+    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)
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "left" and m.voiceBox.isinFocusChain()
+        m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+        m.genreList.setFocus(m.genreList.opacity = 1)
+        m.voiceBox.setFocus(false)
+    end if
+
+    if key = "options"
+        if m.options.visible = true
+            m.options.visible = false
+            m.top.removeChild(m.options)
+            optionsClosed()
+        else
+
+            itemSelected = m.selectedFavoriteItem
+            if itemSelected <> invalid
+                m.options.selectedFavoriteItem = itemSelected
+            end if
+
+            m.options.visible = true
+            m.top.appendChild(m.options)
+            m.options.setFocus(true)
+        end if
+        return true
+    else if key = "back"
+        if m.options.visible = true
+            m.options.visible = false
+            optionsClosed()
+            return true
+        else
+            m.global.sceneManager.callfunc("popScene")
+            m.loadItemsTask.control = "stop"
+            return true
+        end if
+    else if key = "left"
+        if m.itemGrid.isinFocusChain()
+            m.top.alphaActive = true
+            m.itemGrid.setFocus(false)
+            alpha = m.alpha.getChild(0).findNode("Alphamenu")
+            alpha.setFocus(true)
+            return true
+        else if m.genreList.isinFocusChain()
+            m.top.alphaActive = true
+            m.genreList.setFocus(false)
+            alpha = m.alpha.getChild(0).findNode("Alphamenu")
+            alpha.setFocus(true)
+            return true
+        end if
+    else if key = "right" and m.Alpha.isinFocusChain()
+        m.top.alphaActive = false
+        m.Alpha.setFocus(false)
+        m.Alpha.visible = true
+
+        m.itemGrid.setFocus(m.itemGrid.opacity = 1)
+        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
+        else
+            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.loadItemsTask.searchTerm = ""
+        m.loadItemsTask.nameStartsWith = ""
+        m.voiceBox.text = ""
+        m.top.alphaSelected = ""
+        m.loadItemsTask.filter = "All"
+        m.filter = "All"
+        m.data = CreateObject("roSGNode", "ContentNode")
+        m.itemGrid.content = m.data
+        loadInitialItems()
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFButton.bs.html b/docs/api/components_JFButton.bs.html new file mode 100644 index 000000000..14f835c32 --- /dev/null +++ b/docs/api/components_JFButton.bs.html @@ -0,0 +1,27 @@ +Source: components/JFButton.bs
On this page

components_JFButton.bs

sub init()
+    m.top.observeFieldScoped("text", "onTextChanged")
+    m.top.iconUri = ""
+    m.top.focusedIconUri = ""
+    m.top.showFocusFootprint = true
+    m.top.minWidth = 0
+end sub
+
+'
+' Whenever the text changes, pad both sides with whitespace so we can center the button text
+'
+sub onTextChanged()
+    addSpaceAfter = true
+    minChars = m.top.minChars
+    if minChars = invalid then minChars = 50
+    while m.top.text.Len() < minChars
+        if addSpaceAfter
+            m.top.text = m.top.text + Chr(160)
+        else
+            m.top.text = Chr(160) + m.top.text
+        end if
+        addSpaceAfter = addSpaceAfter = false
+    end while
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFGroup.bs.html b/docs/api/components_JFGroup.bs.html new file mode 100644 index 000000000..dc48d614d --- /dev/null +++ b/docs/api/components_JFGroup.bs.html @@ -0,0 +1,11 @@ +Source: components/JFGroup.bs
On this page

components_JFGroup.bs

sub init()
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFMessageDialog.bs.html b/docs/api/components_JFMessageDialog.bs.html new file mode 100644 index 000000000..05e90a00e --- /dev/null +++ b/docs/api/components_JFMessageDialog.bs.html @@ -0,0 +1,76 @@ +Source: components/JFMessageDialog.bs
On this page

components_JFMessageDialog.bs

sub init()
+    options = m.top.findNode("optionList")
+    options.focusBitmapBlendColor = "0x0cb0e8"
+    options.color = "0xffffff"
+    options.focusedColor = "0xffffff"
+    options.setFocus(true)
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if key = "back"
+        m.top.backPressed = true
+        return true
+    end if
+    return false
+end function
+
+sub updateOptions()
+    for each item in m.top.options
+        row = CreateObject("roSGNode", "ContentNode")
+        row.title = item
+        m.top.findNode("content").appendChild(row)
+    end for
+    redraw()
+end sub
+
+sub updateMessage()
+    message = m.top.findNode("messageText")
+    message.text = m.top.message
+    redraw()
+end sub
+
+sub redraw()
+    boxWidth = 900
+    border = 40
+    itemSpacing = 40
+    optionHeight = 60
+    maxRows = 9
+
+    bg = m.top.findNode("dialogBackground")
+    text = m.top.findNode("messageText")
+    options = m.top.findNode("optionList")
+    fontHeight = m.top.fontHeight
+    fontWidth = m.top.fontWidth
+
+    if text.text.len() > 0
+        textWidth = boxWidth - (border * 2)
+        text.width = textWidth
+        text.numLines = int(fontWidth / textWidth) + 1
+        text.translation = [border, border]
+        textHeight = (fontHeight * text.numLines)
+    else
+        textHeight = 0
+        itemSpacing = border
+    end if
+
+    options.translation = [border * 2, textHeight + itemSpacing]
+    options.itemSize = [boxWidth - (border * 4), optionHeight]
+    options.itemSpacing = "[0,20]"
+
+    options.numRows = m.top.options.count()
+    if options.numRows > maxRows
+        options.numRows = maxRows
+        options.wrapDividerHeight = 0
+        options.vertFocusAnimationStyle = "fixedFocusWrap"
+    end if
+
+    boxHeight = options.translation[1] + (options.itemSize[1] * options.numRows) + (options.itemSpacing[1] * (options.NumRows - 1)) + border
+
+    bg.width = boxWidth
+    bg.height = boxHeight
+
+    m.top.translation = [(1920 - boxWidth) / 2, (1080 - boxHeight) / 2]
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFOverhang.bs.html b/docs/api/components_JFOverhang.bs.html new file mode 100644 index 000000000..0dc30a3cb --- /dev/null +++ b/docs/api/components_JFOverhang.bs.html @@ -0,0 +1,150 @@ +Source: components/JFOverhang.bs
On this page

components_JFOverhang.bs

import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.id = "overhang"
+    ' hide seperators till they're needed
+    m.leftSeperator = m.top.findNode("overlayLeftSeperator")
+    m.leftSeperator.visible = "false"
+    m.rightSeperator = m.top.findNode("overlayRightSeperator")
+    ' set font sizes
+    m.optionText = m.top.findNode("overlayOptionsText")
+    m.optionText.font.size = 20
+    m.optionStar = m.top.findNode("overlayOptionsStar")
+    m.optionStar.font.size = 58
+    ' save node references
+    m.title = m.top.findNode("overlayTitle")
+    m.overlayRightGroup = m.top.findNode("overlayRightGroup")
+    m.overlayTimeGroup = m.top.findNode("overlayTimeGroup")
+    m.slideDownAnimation = m.top.findNode("slideDown")
+    m.slideUpAnimation = m.top.findNode("slideUp")
+    ' show clock based on user setting
+    m.hideClock = m.global.session.user.settings["ui.design.hideclock"]
+    if not m.hideClock
+        ' save node references
+        m.overlayHours = m.top.findNode("overlayHours")
+        m.overlayMinutes = m.top.findNode("overlayMinutes")
+        m.overlayMeridian = m.top.findNode("overlayMeridian")
+        m.overlayMeridian.font.size = 20
+        m.currentTimeTimer = m.top.findNode("currentTimeTimer")
+        ' display current time
+        updateTime()
+        ' start timer to update clock every minute
+        m.currentTimeTimer.control = "start"
+        m.currentTimeTimer.ObserveField("fire", "updateTime")
+    end if
+
+    setClockVisibility()
+end sub
+
+sub onVisibleChange()
+    if m.top.disableMoveAnimation
+        m.top.translation = [0, 0]
+        return
+    end if
+    if m.top.isVisible
+        m.slideDownAnimation.control = "start"
+        return
+    end if
+
+    m.slideUpAnimation.control = "start"
+end sub
+
+sub updateTitle()
+    if m.top.title <> ""
+        m.leftSeperator.visible = "true"
+    else
+        m.leftSeperator.visible = "false"
+    end if
+    m.title.text = m.top.title
+
+    if not m.hideClock
+        resetTime()
+    end if
+end sub
+
+sub setClockVisibility()
+    if m.hideClock
+        m.overlayRightGroup.removeChild(m.overlayTimeGroup)
+    end if
+end sub
+
+sub setRightSeperatorVisibility()
+    if m.hideClock
+        m.top.removeChild(m.rightSeperator)
+        return
+    end if
+
+    if m.top.currentUser <> ""
+        m.rightSeperator.visible = "true"
+    else
+        m.rightSeperator.visible = "false"
+    end if
+end sub
+
+sub updateUser()
+    setRightSeperatorVisibility()
+    user = m.top.findNode("overlayCurrentUser")
+    user.text = m.top.currentUser
+end sub
+
+sub updateTime()
+    currentTime = CreateObject("roDateTime")
+    currentTime.ToLocalTime()
+    m.currentTimeTimer.duration = 60 - currentTime.GetSeconds()
+    m.currentHours = currentTime.GetHours()
+    m.currentMinutes = currentTime.GetMinutes()
+    updateTimeDisplay()
+end sub
+
+sub resetTime()
+    if m.hideClock then return
+    m.currentTimeTimer.control = "stop"
+    m.currentTimeTimer.control = "start"
+    updateTime()
+end sub
+
+sub updateTimeDisplay()
+    if m.global.device.clockFormat = "24h"
+        m.overlayMeridian.text = ""
+        if m.currentHours < 10
+            m.overlayHours.text = "0" + StrI(m.currentHours).trim()
+        else
+            m.overlayHours.text = m.currentHours
+        end if
+    else
+        if m.currentHours < 12
+            m.overlayMeridian.text = "AM"
+            if m.currentHours = 0
+                m.overlayHours.text = "12"
+            else
+                m.overlayHours.text = m.currentHours
+            end if
+        else
+            m.overlayMeridian.text = "PM"
+            if m.currentHours = 12
+                m.overlayHours.text = "12"
+            else
+                m.overlayHours.text = m.currentHours - 12
+            end if
+        end if
+    end if
+
+    if m.currentMinutes < 10
+        m.overlayMinutes.text = "0" + StrI(m.currentMinutes).trim()
+    else
+        m.overlayMinutes.text = m.currentMinutes
+    end if
+end sub
+
+sub updateOptions()
+    if m.top.showOptions = true
+        m.optionText.visible = true
+        m.optionStar.visible = true
+    else
+        m.optionText.visible = false
+        m.optionStar.visible = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFScene.bs.html b/docs/api/components_JFScene.bs.html new file mode 100644 index 000000000..b9a4d951e --- /dev/null +++ b/docs/api/components_JFScene.bs.html @@ -0,0 +1,50 @@ +Source: components/JFScene.bs
On this page

components_JFScene.bs

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
+    if not press then return false
+
+    if key = "back"
+        m.global.sceneManager.callFunc("popScene")
+        return true
+    else if key = "options"
+        group = m.global.sceneManager.callFunc("getActiveScene")
+        if isValid(group) and isValid(group.optionsAvailable) and group.optionsAvailable
+            group.lastFocus = group.focusedChild
+            panel = group.findNode("options")
+            panel.visible = true
+            panel.findNode("panelList").setFocus(true)
+        end if
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFScreen.bs.html b/docs/api/components_JFScreen.bs.html new file mode 100644 index 000000000..dbdc69b30 --- /dev/null +++ b/docs/api/components_JFScreen.bs.html @@ -0,0 +1,27 @@ +Source: components/JFScreen.bs
On this page

components_JFScreen.bs

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
+sub OnScreenShown()
+    if m.top.lastFocus <> invalid
+        m.top.lastFocus.setFocus(true)
+    else
+        m.top.setFocus(true)
+    end if
+end sub
+
+' Function called when the screen is hidden by the screen manager
+' It is expected that screens override this function if required,
+' to handle focus any actions required on the screen being hidden
+sub OnScreenHidden()
+end sub
+
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_JFVideo.bs.html b/docs/api/components_JFVideo.bs.html new file mode 100644 index 000000000..bf96dc6bf --- /dev/null +++ b/docs/api/components_JFVideo.bs.html @@ -0,0 +1,316 @@ +Source: components/JFVideo.bs
On this page

components_JFVideo.bs

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")
+    m.top.observeField("state", "onState")
+    m.top.observeField("content", "onContentChange")
+
+    m.playbackTimer.observeField("fire", "ReportPlayback")
+    m.bufferPercentage = 0 ' Track whether content is being loaded
+    m.playReported = false
+    m.top.transcodeReasons = []
+    m.bufferCheckTimer.duration = 30
+
+    if 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
+
+    'Play Next Episode button
+    m.nextEpisodeButton = m.top.findNode("nextEpisode")
+    m.nextEpisodeButton.text = tr("Next Episode")
+    m.nextEpisodeButton.setFocus(false)
+    m.nextupbuttonseconds = m.global.session.user.settings["playback.nextupbuttonseconds"].ToInt()
+
+    m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
+    m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
+
+    m.checkedForNextEpisode = false
+    m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
+    m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
+
+    m.top.observeField("allowCaptions", "onAllowCaptionsChange")
+end sub
+
+sub onAllowCaptionsChange()
+    if not m.top.allowCaptions then return
+
+    m.captionGroup = m.top.findNode("captionGroup")
+    m.captionGroup.createchildren(9, "LayoutGroup")
+    m.captionTask = createObject("roSGNode", "captionTask")
+    m.captionTask.observeField("currentCaption", "updateCaption")
+    m.captionTask.observeField("useThis", "checkCaptionMode")
+    m.top.observeField("currentSubtitleTrack", "loadCaption")
+    m.top.observeField("globalCaptionMode", "toggleCaption")
+    if m.global.session.user.settings["playback.subs.custom"] = false
+        m.top.suppressCaptions = false
+    else
+        m.top.suppressCaptions = true
+        toggleCaption()
+    end if
+end sub
+
+sub loadCaption()
+    if m.top.suppressCaptions
+        m.captionTask.url = m.top.currentSubtitleTrack
+    end if
+end sub
+
+sub toggleCaption()
+    m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
+    if LCase(m.top.globalCaptionMode) = "on"
+        m.captionTask.playerState = m.top.state + m.top.globalCaptionMode + "w"
+        m.captionGroup.visible = true
+    else
+        m.captionGroup.visible = false
+    end if
+end sub
+
+sub updateCaption ()
+    m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0)
+    m.captionGroup.appendChildren(m.captionTask.currentCaption)
+end sub
+
+' Event handler for when video content field changes
+sub onContentChange()
+    if not isValid(m.top.content) then return
+
+    m.top.observeField("position", "onPositionChanged")
+
+end sub
+
+sub onNextEpisodeDataLoaded()
+    m.checkedForNextEpisode = true
+
+    m.top.observeField("position", "onPositionChanged")
+end sub
+
+'
+' Runs Next Episode button animation and sets focus to button
+sub showNextEpisodeButton()
+    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)
+    end if
+end sub
+
+'
+'Update count down text
+sub updateCount()
+    nextEpisodeCountdown = Int(m.top.duration - m.top.position)
+    if nextEpisodeCountdown < 0
+        nextEpisodeCountdown = 0
+    end if
+    m.nextEpisodeButton.text = tr("Next Episode") + " " + nextEpisodeCountdown.toStr()
+end sub
+
+'
+' Runs hide Next Episode button animation and sets focus back to video
+sub hideNextEpisodeButton()
+    m.hideNextEpisodeButtonAnimation.control = "start"
+    m.nextEpisodeButton.setFocus(false)
+    m.top.setFocus(true)
+end sub
+
+' Checks if we need to display the Next Episode button
+sub checkTimeToDisplayNextEpisode()
+    if 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 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()
+        m.nextEpisodeButton.visible = false
+        m.nextEpisodeButton.setFocus(false)
+    end if
+end sub
+
+' When Video Player state changes
+sub onPositionChanged()
+    if isValid(m.captionTask)
+        m.captionTask.currentPos = Int(m.top.position * 1000)
+    end if
+    ' Check if dialog is open
+    m.dialog = m.top.getScene().findNode("dialogBackground")
+    if not isValid(m.dialog)
+        checkTimeToDisplayNextEpisode()
+    end if
+end sub
+
+'
+' When Video Player state changes
+sub onState(msg)
+    if isValid(m.captionTask)
+        m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
+    end if
+    ' When buffering, start timer to monitor buffering process
+    if m.top.state = "buffering" and m.bufferCheckTimer <> invalid
+
+        ' start timer
+        m.bufferCheckTimer.control = "start"
+        m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
+    else if m.top.state = "error"
+        if not m.playReported and m.top.transcodeAvailable
+            m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding
+        else
+            ' If an error was encountered, Display dialog
+            dialog = createObject("roSGNode", "PlaybackDialog")
+            dialog.title = tr("Error During Playback")
+            dialog.buttons = [tr("OK")]
+            dialog.message = tr("An error was encountered while playing this item.")
+            m.top.getScene().dialog = dialog
+        end if
+
+        ' Stop playback and exit player
+        m.top.control = "stop"
+        m.top.backPressed = true
+    else if m.top.state = "playing"
+        ' Check if next episde is available
+        if isValid(m.top.showID)
+            if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
+                m.getNextEpisodeTask.showID = m.top.showID
+                m.getNextEpisodeTask.videoID = m.top.id
+                m.getNextEpisodeTask.control = "RUN"
+            end if
+        end if
+
+        if m.playReported = false
+            ReportPlayback("start")
+            m.playReported = true
+        else
+            ReportPlayback()
+        end if
+        m.playbackTimer.control = "start"
+    else if m.top.state = "paused"
+        m.playbackTimer.control = "stop"
+        ReportPlayback()
+    else if m.top.state = "stopped"
+        m.playbackTimer.control = "stop"
+        ReportPlayback("stop")
+        m.playReported = false
+    end if
+
+end sub
+
+'
+' Report playback to server
+sub ReportPlayback(state = "update" as string)
+
+    if m.top.position = invalid then return
+
+    params = {
+        "ItemId": m.top.id,
+        "PlaySessionId": m.top.PlaySessionId,
+        "PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
+        "IsPaused": (m.top.state = "paused")
+    }
+    if m.top.content.live
+        params.append({
+            "MediaSourceId": m.top.transcodeParams.MediaSourceId,
+            "LiveStreamId": m.top.transcodeParams.LiveStreamId
+        })
+        m.bufferCheckTimer.duration = 30
+    end if
+
+    ' Report playstate via worker task
+    playstateTask = m.global.playstateTask
+    playstateTask.setFields({ status: state, params: params })
+    playstateTask.control = "RUN"
+end sub
+
+'
+' Check the the buffering has not hung
+sub bufferCheck(msg)
+
+    if m.top.state <> "buffering"
+        ' If video is not buffering, stop timer
+        m.bufferCheckTimer.control = "stop"
+        m.bufferCheckTimer.unobserveField("fire")
+        return
+    end if
+    if m.top.bufferingStatus <> invalid
+
+        ' Check that the buffering percentage is increasing
+        if m.top.bufferingStatus["percentage"] > m.bufferPercentage
+            m.bufferPercentage = m.top.bufferingStatus["percentage"]
+        else if m.top.content.live = true
+            m.top.callFunc("refresh")
+        else
+            ' If buffering has stopped Display dialog
+            dialog = createObject("roSGNode", "PlaybackDialog")
+            dialog.title = tr("Error Retrieving Content")
+            dialog.buttons = [tr("OK")]
+            dialog.message = tr("There was an error retrieving the data for this item from the server.")
+            m.top.getScene().dialog = dialog
+
+            ' Stop playback and exit player
+            m.top.control = "stop"
+            m.top.backPressed = true
+        end if
+    end if
+
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
+        m.top.state = "finished"
+        hideNextEpisodeButton()
+        return true
+    else
+        'Hide Next Episode Button
+        if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus()
+            m.nextEpisodeButton.opacity = 0
+            m.nextEpisodeButton.setFocus(false)
+            m.top.setFocus(true)
+        end if
+    end if
+
+    if not press then return false
+
+    if key = "down"
+        m.top.selectSubtitlePressed = true
+        return true
+    else if key = "up"
+        m.top.selectPlaybackInfoPressed = true
+        return true
+    else if key = "OK"
+        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"
+            m.top.control = "pause"
+            return false
+        end if
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_ListPoster.bs.html b/docs/api/components_ListPoster.bs.html new file mode 100644 index 000000000..998d63097 --- /dev/null +++ b/docs/api/components_ListPoster.bs.html @@ -0,0 +1,119 @@ +Source: components/ListPoster.bs
On this page

components_ListPoster.bs

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")
+    m.series = m.top.findNode("Series")
+    m.poster = m.top.findNode("poster")
+    m.unplayedCount = m.top.findNode("unplayedCount")
+    m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
+
+    m.backdrop = m.top.findNode("backdrop")
+
+    ' Randmomise the background colors
+    posterBackgrounds = m.global.constants.poster_bg_pallet
+    m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
+
+    updateSize()
+end sub
+
+sub updateSize()
+    image = invalid
+    if isValid(m.top.itemContent) and isValid(m.top.itemContent.image)
+        image = m.top.itemContent.image
+    end if
+
+    if image = invalid
+        m.backdrop.visible = true
+    else
+        m.backdrop.visible = false
+    end if
+
+    ' TODO - abstract this in case the parent doesnt have itemSize
+    maxSize = m.top.getParent().itemSize
+
+    ' Always reserve the bottom for the Poster Title
+    m.title.maxWidth = maxSize[0]
+    m.title.height = 40
+    m.title.translation = [0, int(maxSize[1]) - m.title.height + 5]
+
+    m.staticTitle.width = maxSize[0]
+    m.staticTitle.height = m.title.height
+    m.staticTitle.translation = m.title.translation
+
+    m.series.maxWidth = maxSize[0]
+
+    m.poster.width = int(maxSize[0]) - 4
+    m.poster.height = int(maxSize[1]) - m.title.height 'Set poster height to available space
+
+    m.backdrop.width = m.poster.width
+    m.backdrop.height = m.poster.height
+end sub
+
+sub itemContentChanged() as void
+    m.poster = m.top.findNode("poster")
+    itemData = m.top.itemContent
+    m.title.text = itemData.title
+
+    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
+                m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+            end if
+        end if
+    end if
+
+    if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.IndexNumber)
+        m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text
+
+        m.series.text = itemData.json.Series
+        m.series.visible = true
+    else if itemData.json.lookup("Type") = "MusicAlbum"
+        m.title.font = "font:SmallestSystemFont"
+        m.staticTitle.font = "font:SmallestSystemFont"
+    else
+        m.series.visible = false
+    end if
+    m.staticTitle.text = m.title.text
+
+    imageUrl = itemData.posterURL
+
+    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"
+            end if
+        end if
+    end if
+
+    m.poster.uri = imageUrl
+
+    updateSize()
+end sub
+
+'
+' Enable title scrolling based on item Focus
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        m.title.repeatCount = -1
+        m.series.repeatCount = -1
+        m.staticTitle.visible = false
+        m.title.visible = true
+        ' text to speech for accessibility
+        if m.global.device.isAudioGuideEnabled = true
+            txt2Speech = CreateObject("roTextToSpeech")
+            txt2Speech.Flush()
+            txt2Speech.Say(m.title.text)
+        end if
+    else
+        m.title.repeatCount = 0
+        m.series.repeatCount = 0
+        m.staticTitle.visible = true
+        m.title.visible = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_OverviewDialog.bs.html b/docs/api/components_OverviewDialog.bs.html new file mode 100644 index 000000000..4ebb792a2 --- /dev/null +++ b/docs/api/components_OverviewDialog.bs.html @@ -0,0 +1,20 @@ +Source: components/OverviewDialog.bs
On this page

components_OverviewDialog.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_PersonDetails.bs.html b/docs/api/components_PersonDetails.bs.html new file mode 100644 index 000000000..7a3fbf72b --- /dev/null +++ b/docs/api/components_PersonDetails.bs.html @@ -0,0 +1,177 @@ +Source: components/PersonDetails.bs
On this page

components_PersonDetails.bs

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")
+    m.btnGrp = m.top.findNode("buttons")
+    m.btnGrp.observeField("escape", "onButtonGroupEscaped")
+    m.favBtn = m.top.findNode("favorite-button")
+    m.extrasGrp = m.top.findNode("extrasGrp")
+    m.extrasGrp.opacity = 1.0
+    createDialogPallete()
+    m.top.optionsAvailable = false
+end sub
+
+sub loadPerson()
+    item = m.top.itemContent
+    itemData = item.json
+    m.top.Id = itemData.id
+    name = m.top.findNode("Name")
+    name.Text = itemData.Name
+    name.font.size = 70
+    if itemData.PremiereDate <> invalid and itemData.PremiereDate <> ""
+        lifeStringLabel = createObject("rosgnode", "Label")
+        lifeStringLabel.id = "premierDate"
+        lifeStringLabel.font = "font:SmallestBoldSystemFont"
+        lifeStringLabel.height = "100"
+        lifeStringLabel.vertAlign = "bottom"
+        name.vertAlign = "top"
+        name.font.size = 60
+        m.top.findNode("title_rectangle").appendChild(lifeStringLabel)
+        birthDate = CreateObject("roDateTime")
+        birthDate.FromISO8601String(itemData.PremiereDate)
+        deathDate = CreateObject("roDatetime")
+        lifeString = tr("Born") + ": " + birthDate.AsDateString("short-month-no-weekday")
+
+        if itemData.EndDate <> invalid and itemData.EndDate <> ""
+            deathDate.FromISO8601String(itemData.EndDate)
+            lifeString = lifeString + " * " + tr("Died") + ": " + deathDate.AsDateString("short-month-no-weekday")
+
+        end if
+        ' Calculate age
+        age = deathDate.getYear() - birthDate.getYear()
+        if deathDate.getMonth() < birthDate.getMonth()
+            age--
+        else if deathDate.getMonth() = birthDate.getMonth()
+            if deathDate.getDayOfMonth() < birthDate.getDayOfMonth()
+                age--
+            end if
+        end if
+        lifeString = lifeString + " * " + tr("Age") + ": " + stri(age)
+        lifeStringLabel.Text = lifeString
+    end if
+    if itemData.Overview <> invalid and itemData.Overview <> ""
+        m.dscr.text = itemData.Overview
+    else
+        m.dscr.text = tr("Biographical information for this person is not currently available.")
+        m.dscr.horizAlign = "center"
+        m.dscr.vertAlign = "center"
+    end if
+    if item.posterURL <> invalid and item.posterURL <> ""
+        m.top.findnode("personImage").uri = item.posterURL
+    else
+        m.top.findnode("personImage").uri = "pkg:/images/baseline_person_white_48dp.png"
+    end if
+    m.vidsList.callFunc("loadPersonVideos", m.top.Id)
+
+    setFavoriteColor()
+    if not m.favBtn.hasFocus() then dscrShowFocus()
+end sub
+
+sub dscrShowFocus()
+    m.dscr.setFocus(true)
+    m.dscr.opacity = 1.0
+    m.top.findNode("dscrBorder").color = "#d0d0d0ff"
+end sub
+
+sub onButtonGroupEscaped()
+    key = m.btnGrp.escape
+    if key = "down"
+        m.dscr.setFocus(true)
+        m.dscr.opacity = 1.0
+        m.top.findNode("dscrBorder").color = "#d0d0d0ff"
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "OK"
+        if m.dscr.hasFocus()
+            createFullDscrDlg()
+            return true
+        end if
+        return false
+    end if
+
+    if key = "back"
+        m.global.sceneManager.callfunc("popScene")
+        return true
+    end if
+
+    if key = "down"
+        if m.dscr.hasFocus()
+            m.dscr.opacity = 0.6
+            m.top.findNode("dscrBorder").color = "#data202020ff"
+            m.vidsList.setFocus(true)
+            m.top.findNode("VertSlider").reverse = false
+            m.top.findNode("pplAnime").control = "start"
+            return true
+        end if
+    else if key = "up"
+        if m.dscr.hasFocus()
+            m.favBtn.setFocus(true)
+            m.dscr.opacity = 0.6
+            m.top.findNode("dscrBorder").color = "#data202020ff"
+            return true
+        else if m.vidsList.isInFocusChain() and m.vidsList.itemFocused = 0
+            m.top.findNode("VertSlider").reverse = true
+            m.top.findNode("pplAnime").control = "start"
+            dscrShowFocus()
+            return true
+        end if
+    end if
+    return false
+end function
+
+sub setFavoriteColor()
+    fave = m.top.itemContent.favorite
+    fave_button = m.top.findNode("favorite-button")
+    if fave <> invalid and fave
+        fave_button.textColor = "#00ff00ff"
+        fave_button.focusedTextColor = "#269926ff"
+        fave_button.text = tr("Favorite")
+    else
+        fave_button.textColor = "0xddddddff"
+        fave_button.focusedTextColor = "#262626ff"
+        fave_button.text = tr("Set Favorite")
+    end if
+end sub
+
+sub createFullDscrDlg()
+    dlg = CreateObject("roSGNode", "OverviewDialog")
+    dlg.Title = m.top.itemContent.json.Name
+    dlg.width = 1290
+    dlg.palette = m.dlgPalette
+    dlg.overview = m.dscr.text
+    m.fullDscrDlg = dlg
+    m.top.getScene().dialog = dlg
+
+end sub
+
+sub createDialogPallete()
+    m.dlgPalette = createObject("roSGNode", "RSGPalette")
+    m.dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogItemColor: "0x00EF00FF",
+        DialogTextColor: "0xb0b0b0FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "0xcc7ecc4D",
+        DialogInputFieldColor: "0x80FF8080",
+        KeyboardDialogColor: "0x80FF804D",
+        DialogFootprintColor: "0x80FF804D"
+    }
+end sub
+
+function shortDate(isoDate) as string
+    myDate = CreateObject("roDateTime")
+    myDate.FromISO8601String(isoDate)
+    return myDate.AsDateString("short-month-no-weekday")
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_PlaybackDialog.bs.html b/docs/api/components_PlaybackDialog.bs.html new file mode 100644 index 000000000..74390d230 --- /dev/null +++ b/docs/api/components_PlaybackDialog.bs.html @@ -0,0 +1,12 @@ +Source: components/PlaybackDialog.bs
On this page

components_PlaybackDialog.bs

function onKeyEvent(key as string, press as boolean) as boolean
+
+    if key = "OK"
+        m.top.close = true
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_PlayedCheckmark.bs.html b/docs/api/components_PlayedCheckmark.bs.html new file mode 100644 index 000000000..ee11e3d64 --- /dev/null +++ b/docs/api/components_PlayedCheckmark.bs.html @@ -0,0 +1,7 @@ +Source: components/PlayedCheckmark.bs
On this page

components_PlayedCheckmark.bs

sub init()
+    checkmark = m.top.findNode("checkmark")
+    checkmark.font.size = 48
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_PlaystateTask.bs.html b/docs/api/components_PlaystateTask.bs.html new file mode 100644 index 000000000..9224a858f --- /dev/null +++ b/docs/api/components_PlaystateTask.bs.html @@ -0,0 +1,57 @@ +Source: components/PlaystateTask.bs
On this page

components_PlaystateTask.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_RadioDialog.bs.html b/docs/api/components_RadioDialog.bs.html new file mode 100644 index 000000000..b677f3478 --- /dev/null +++ b/docs/api/components_RadioDialog.bs.html @@ -0,0 +1,141 @@ +Source: components/RadioDialog.bs
On this page

components_RadioDialog.bs

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
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_SearchBox.bs.html b/docs/api/components_SearchBox.bs.html new file mode 100644 index 000000000..142fc1b8f --- /dev/null +++ b/docs/api/components_SearchBox.bs.html @@ -0,0 +1,36 @@ +Source: components/SearchBox.bs
On this page

components_SearchBox.bs

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"
+    m.top.vertAlignment = "top"
+    m.top.visible = false
+    m.searchText = m.top.findNode("search_Key")
+    m.searchText.textEditBox.hintText = tr("Search")
+    m.searchText.keyGrid.keyDefinitionUri = "pkg:/components/data/CustomAddressKDF.json"
+    m.searchText.textEditBox.voiceEnabled = true
+    m.searchText.textEditBox.active = true
+    m.searchText.ObserveField("text", "searchMedias")
+    m.searchSelect = m.top.findNode("searchSelect")
+
+    'set lable text
+    m.label = m.top.findNode("text")
+    m.label.text = tr("Search now")
+
+end sub
+
+sub searchMedias()
+    m.top.search_values = m.searchText.text
+    if m.top.search_values.len() > 1
+        m.searchText.textEditBox.leadingEllipsis = true
+    else
+        m.searchText.textEditBox.leadingEllipsis = false
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_Spinner.bs.html b/docs/api/components_Spinner.bs.html new file mode 100644 index 000000000..704a16861 --- /dev/null +++ b/docs/api/components_Spinner.bs.html @@ -0,0 +1,9 @@ +Source: components/Spinner.bs
On this page

components_Spinner.bs

sub init()
+    m.top.poster.uri = "pkg:/images/spinner.png"
+    m.top.control = "start"
+    m.top.clockwise = true
+    m.top.spinInterval = 1
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_StandardDialog.bs.html b/docs/api/components_StandardDialog.bs.html new file mode 100644 index 000000000..e35837e15 --- /dev/null +++ b/docs/api/components_StandardDialog.bs.html @@ -0,0 +1,39 @@ +Source: components/StandardDialog.bs
On this page

components_StandardDialog.bs

sub init()
+    m.content = m.top.findNode("content")
+    m.top.observeField("contentData", "onContentDataChanged")
+
+    m.top.id = "OKDialog"
+    m.top.height = 900
+    m.top.title = "What's New?"
+    m.top.buttons = [tr("OK")]
+
+    m.dialogStyles = {
+        "default": {
+            "fontSize": 27,
+            "fontUri": "font:BoldSystemFontFile",
+            "color": "#EFEFEFFF"
+        },
+        "b": {
+            "fontSize": 27,
+            "fontUri": "font:SystemFontFile",
+            "color": "#999999"
+        },
+        "header": {
+            "fontSize": 35,
+            "fontUri": "font:SystemFontFile",
+            "color": "#00a4dcFF"
+        }
+    }
+
+end sub
+
+sub onContentDataChanged()
+    for each item in m.top.contentData.data
+        textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")
+        textLine.drawingStyles = m.dialogStyles
+        textLine.text = item
+    end for
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_WhatsNewDialog.bs.html b/docs/api/components_WhatsNewDialog.bs.html new file mode 100644 index 000000000..762f8915d --- /dev/null +++ b/docs/api/components_WhatsNewDialog.bs.html @@ -0,0 +1,49 @@ +Source: components/WhatsNewDialog.bs
On this page

components_WhatsNewDialog.bs

sub init()
+    m.content = m.top.findNode("content")
+
+    setPalette()
+
+    m.top.id = "OKDialog"
+    m.top.height = 900
+    m.top.title = m.global.app.version + " - " + tr("What's New?")
+    m.top.buttons = [tr("OK")]
+
+    dialogStyles = {
+        "default": {
+            "fontSize": 27,
+            "fontUri": "font:SystemFontFile",
+            "color": "#EFEFEFFF"
+        },
+        "author": {
+            "fontSize": 27,
+            "fontUri": "font:SystemFontFile",
+            "color": "#00a4dcFF"
+        }
+    }
+
+    whatsNewList = ParseJSON(ReadAsciiFile("pkg:/source/static/whatsNew/" + m.global.app.version.ToStr().trim() + ".json"))
+
+    for each item in whatsNewList
+        textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")
+        textLine.drawingStyles = dialogStyles
+        textLine.text = "โ€ข " + item.description + " <author>" + item.author + "</author>"
+    end for
+
+end sub
+
+sub setPalette()
+    dlgPalette = createObject("roSGNode", "RSGPalette")
+    dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "#00a4dcFF",
+        DialogTextColor: "0xeeeeeeFF"
+    }
+
+    m.top.palette = dlgPalette
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_captionTask.bs.html b/docs/api/components_captionTask.bs.html new file mode 100644 index 000000000..3c0835115 --- /dev/null +++ b/docs/api/components_captionTask.bs.html @@ -0,0 +1,153 @@ +Source: components/captionTask.bs
On this page

components_captionTask.bs

import "pkg:/source/utils/config.bs"
+import "pkg:/source/api/baserequest.bs"
+
+sub init()
+    m.top.observeField("url", "fetchCaption")
+    m.top.currentCaption = []
+    m.top.currentPos = 0
+
+    m.captionTimer = m.top.findNode("captionTimer")
+    m.captionTimer.ObserveField("fire", "updateCaption")
+
+    m.captionList = []
+    m.reader = createObject("roUrlTransfer")
+    m.font = CreateObject("roSGNode", "Font")
+    m.tags = CreateObject("roRegex", "{\\an\d*}|&lt;.*?&gt;|<.*?>", "s")
+
+    ' Caption Style
+    m.fontSizeDict = { "Default": 60, "Large": 60, "Extra Large": 70, "Medium": 50, "Small": 40 }
+    m.percentageDict = { "Default": 1.0, "100%": 1.0, "75%": 0.75, "50%": 0.5, "25%": 0.25, "Off": 0 }
+    m.textColorDict = { "Default": &HFFFFFFFF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }
+    m.bgColorDict = { "Default": &H000000FF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }
+
+    deviceInfo = CreateObject("roDeviceInfo")
+    m.fontSize = m.fontSizeDict[deviceInfo.GetCaptionsOption("Text/Size")]
+    m.textColor = m.textColorDict[deviceInfo.GetCaptionsOption("Text/Color")]
+    m.textOpac = m.percentageDict[deviceInfo.GetCaptionsOption("Text/Opacity")]
+    m.bgColor = m.bgColorDict[deviceInfo.GetCaptionsOption("Background/Color")]
+    m.bgOpac = m.percentageDict[deviceInfo.GetCaptionsOption("Background/Opacity")]
+    setFont()
+end sub
+
+sub setFont()
+    fs = CreateObject("roFileSystem")
+
+    if fs.Exists("tmp:/font")
+        m.font.uri = "tmp:/font"
+        m.font.size = m.fontSize
+    else
+        m.font = "font:LargeSystemFont"
+    end if
+end sub
+
+sub fetchCaption()
+    m.captionTimer.control = "stop"
+    re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
+    url = re.match(m.top.url)[0]
+    if url <> invalid
+        m.reader.setUrl(url)
+        text = m.reader.GetToString()
+        m.captionList = parseVTT(text)
+        m.captionTimer.control = "start"
+    else
+        m.captionTimer.control = "stop"
+    end if
+end sub
+
+function newlabel(txt)
+    label = CreateObject("roSGNode", "Label")
+    label.text = txt
+    label.font = m.font
+    label.font.size = m.fontSize
+    label.color = m.textColor
+    label.opacity = m.textOpac
+    return label
+end function
+
+function newLayoutGroup(labels)
+    newlg = CreateObject("roSGNode", "LayoutGroup")
+    newlg.appendchildren(labels)
+    newlg.horizalignment = "center"
+    newlg.vertalignment = "bottom"
+    return newlg
+end function
+
+function newRect(lg)
+    rectLG = CreateObject("roSGNode", "LayoutGroup")
+    rectxy = lg.BoundingRect()
+    rect = CreateObject("roSGNode", "Rectangle")
+    rect.color = m.bgColor
+    rect.opacity = m.bgOpac
+    rect.width = rectxy.width + 50
+    rect.height = rectxy.height
+    if lg.getchildCount() = 0
+        rect.width = 0
+        rect.height = 0
+    end if
+    rectLG.translation = [0, -rect.height / 2]
+    rectLG.horizalignment = "center"
+    rectLG.vertalignment = "center"
+    rectLG.appendchild(rect)
+    return rectLG
+end function
+
+
+sub updateCaption()
+    m.top.currentCaption = []
+    if LCase(m.top.playerState) = "playingon"
+        m.top.currentPos = m.top.currentPos + 100
+        texts = []
+        for each entry in m.captionList
+            if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"]
+                t = m.tags.replaceAll(entry["text"], "")
+                texts.push(t)
+            end if
+        end for
+        labels = []
+        for each text in texts
+            labels.push(newlabel (text))
+        end for
+        lines = newLayoutGroup(labels)
+        rect = newRect(lines)
+        m.top.currentCaption = [rect, lines]
+    else if LCase(m.top.playerState.right(1)) = "w"
+        m.top.playerState = m.top.playerState.left(len (m.top.playerState) - 1)
+    end if
+end sub
+
+function isTime(text)
+    return text.right(1) = chr(31)
+end function
+
+function toMs(t)
+    t = t.replace(".", ":")
+    t = t.left(12)
+    timestamp = t.tokenize(":")
+    return 3600000 * timestamp[0].toint() + 60000 * timestamp[1].toint() + 1000 * timestamp[2].toint() + timestamp[3].toint()
+end function
+
+function parseVTT(lines)
+    lines = lines.replace(" --> ", chr(31) + chr(10))
+    lines = lines.split(chr(10))
+    curStart = -1
+    curEnd = -1
+    entries = []
+
+    for i = 0 to lines.count() - 1
+        if isTime(lines[i])
+            curStart = toMs (lines[i])
+            curEnd = toMs (lines[i + 1])
+            i += 1
+        else if curStart <> -1
+            trimmed = lines[i].trim()
+            if trimmed <> chr(0)
+                entry = { "start": curStart, "end": curEnd, "text": trimmed }
+                entries.push(entry)
+            end if
+        end if
+    end for
+    return entries
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_ConfigData.bs.html b/docs/api/components_config_ConfigData.bs.html new file mode 100644 index 000000000..6bb11b8a9 --- /dev/null +++ b/docs/api/components_config_ConfigData.bs.html @@ -0,0 +1,5 @@ +Source: components/config/ConfigData.bs
On this page

components_config_ConfigData.bs

sub init()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_ConfigItem.bs.html b/docs/api/components_config_ConfigItem.bs.html new file mode 100644 index 000000000..5d891006e --- /dev/null +++ b/docs/api/components_config_ConfigItem.bs.html @@ -0,0 +1,40 @@ +Source: components/config/ConfigItem.bs
On this page

components_config_ConfigItem.bs

sub init()
+    m.name = m.top.findNode("label")
+    m.value = m.top.findNode("value")
+
+    m.name.width = 240
+    m.name.height = 75
+
+    m.name.vertAlign = "center"
+    m.name.horizAlign = "center"
+
+    m.value.hintText = tr("Enter a username")
+    m.value.maxTextLength = 120
+end sub
+
+sub itemContentChanged()
+    data = m.top.itemContent
+
+    m.name.text = data.label
+    if data.type = "password"
+        m.value.hintText = tr("Enter a password")
+        m.value.secureMode = true
+    end if
+
+    m.value.text = data.value
+end sub
+
+sub setColors()
+    if m.top.itemHasFocus
+        color = "#101010FF"
+    else
+        color = "#ffffffFF"
+    end if
+
+    m.name.color = color
+    m.value.textColor = color
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_ConfigList.bs.html b/docs/api/components_config_ConfigList.bs.html new file mode 100644 index 000000000..2bb89f952 --- /dev/null +++ b/docs/api/components_config_ConfigList.bs.html @@ -0,0 +1,79 @@ +Source: components/config/ConfigList.bs
On this page

components_config_ConfigList.bs

import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.itemComponentName = "ConfigItem"
+
+    m.top.drawFocusFeedback = True
+    m.top.vertFocusAnimationStyle = "floatingFocus"
+
+    m.top.observeField("itemSelected", "onItemSelected")
+
+    m.top.itemSize = [750, 75]
+    m.top.itemSpacing = [0, 25]
+
+    m.top.setfocus(true)
+
+end sub
+
+sub setData()
+    items = m.top.configItems
+    data = CreateObject("roSGNode", "ContentNode")
+    data.appendChildren(items)
+
+    m.top.content = data
+end sub
+
+sub onItemSelected()
+    i = m.top.itemSelected
+    itemField = m.top.content.getchild(i)
+
+    configListShowDialog(itemField)
+end sub
+
+function onDialogButton()
+    d = m.dialog
+    button_text = d.buttons[d.buttonSelected]
+
+    if button_text = tr("OK")
+        m.configField.value = d.text
+        dismiss_dialog()
+        return true
+    else if button_text = tr("Cancel")
+        dismiss_dialog()
+        return true
+    end if
+    return false
+end function
+
+
+sub configListShowDialog(configField)
+    dialog = createObject("roSGNode", "StandardKeyboardDialog")
+    m.configField = configField
+    dialog.title = configField.label
+    dialog.buttons = [tr("OK"), tr("Cancel")]
+    m.greenPalette = createObject("roSGNode", "RSGPalette")
+    m.greenPalette.colors = {
+        DialogBackgroundColor: "#2A2B2A"
+    }
+    dialog.palette = m.greenPalette
+
+    if configField.type = "password"
+        dialog.textEditBox.secureMode = true
+    end if
+
+    if configField.value <> ""
+        dialog.text = configField.value
+    end if
+
+    m.top.getscene().dialog = dialog
+    m.dialog = dialog
+
+    dialog.observeField("buttonSelected", "onDialogButton")
+end sub
+
+sub dismiss_dialog()
+    m.dialog.close = true
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_JFServer.bs.html b/docs/api/components_config_JFServer.bs.html new file mode 100644 index 000000000..00100ba0d --- /dev/null +++ b/docs/api/components_config_JFServer.bs.html @@ -0,0 +1,37 @@ +Source: components/config/JFServer.bs
On this page

components_config_JFServer.bs

sub init() as void
+    m.poster = m.top.findNode("poster")
+    m.name = m.top.findNode("name")
+    m.baseUrl = m.top.findNode("baseUrl")
+    m.labels = m.top.findNode("labels")
+    setTextColor(0)
+end sub
+
+sub itemContentChanged() as void
+    server = m.top.itemContent
+
+    m.poster.uri = server.iconUrl
+    m.name.text = server.name
+    m.baseUrl.text = server.baseUrl
+end sub
+
+sub onFocusPercentChange(event)
+    setTextColor(event.getData())
+end sub
+
+sub setTextColor(percentFocused)
+    white = "0xffffffff"
+    black = "0x00000099"
+    if percentFocused > .4
+        color = black
+    else
+        color = white
+    end if
+
+    children = m.labels.getChildren(-1, 0)
+    for each child in children
+        child.color = color
+    end for
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_LoginScene.bs.html b/docs/api/components_config_LoginScene.bs.html new file mode 100644 index 000000000..aaac8008d --- /dev/null +++ b/docs/api/components_config_LoginScene.bs.html @@ -0,0 +1,49 @@ +Source: components/config/LoginScene.bs
On this page

components_config_LoginScene.bs

sub init()
+    m.top.setFocus(true)
+    m.top.optionsAvailable = false
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    ' Returns true if user navigates to a new focusable element
+    if not press then return false
+
+    list = m.top.findNode("configOptions")
+    checkbox = m.top.findNode("onOff")
+    submit = m.top.findNode("submit")
+    quickConnect = m.top.findNode("quickConnect")
+    if key = "back"
+        m.top.backPressed = true
+    else if key = "down" and checkbox.focusedChild = invalid and submit.focusedChild = invalid
+        limit = list.content.getChildren(-1, 0).count() - 1
+
+        if limit = list.itemFocused
+            checkbox.setFocus(true)
+            return true
+        end if
+    else if key = "down" and submit.focusedChild = invalid
+        submit.setFocus(true)
+        return true
+    else if key = "up" and submit.focusedChild <> invalid
+        checkbox.setFocus(true)
+        return true
+    else if key = "up" and quickConnect.focusedChild <> invalid
+        checkbox.setFocus(true)
+        return true
+    else if key = "up" and checkbox.focusedChild <> invalid
+        list.setFocus(true)
+        return true
+    else if key = "right" and checkbox.focusedChild <> invalid
+        quickConnect.setFocus(true)
+        return true
+    else if key = "right" and submit.focusedChild <> invalid
+        quickConnect.setFocus(true)
+        return true
+    else if key = "left" and quickConnect.focusedChild <> invalid
+        submit.setFocus(true)
+        return true
+    end if
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_ServerDiscoveryTask.bs.html b/docs/api/components_config_ServerDiscoveryTask.bs.html new file mode 100644 index 000000000..1598f2355 --- /dev/null +++ b/docs/api/components_config_ServerDiscoveryTask.bs.html @@ -0,0 +1,174 @@ +Source: components/config/ServerDiscoveryTask.bs
On this page

components_config_ServerDiscoveryTask.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"
+
+'
+' Task used to discover jellyfin servers on the local network
+'
+sub init()
+    m.log = log.Logger("ServerDiscoveryTask")
+    m.top.functionName = "execute"
+end sub
+
+sub execute()
+    m.servers = []
+    m.serverUrlMap = {}
+    m.locationUrlMap = {}
+    'send both requests at the same time
+    SendSSDPBroadcast()
+    SendClientDiscoveryBroadcast()
+
+    ts = CreateObject("roTimespan")
+    maxTimeMs = 2200
+
+    'monitor each port and collect messages
+    while True
+        elapsed = ts.TotalMilliseconds()
+        if elapsed >= maxTimeMs
+            exit while
+        end if
+
+        msg = Wait(100, m.ssdp.port)
+        if msg <> invalid
+            ProcessSSDPResponse(msg)
+        end if
+
+        msg = Wait(100, m.clientDiscovery.port)
+        if msg <> invalid
+            ProcessClientDiscoveryResponse(msg)
+        end if
+
+    end while
+
+    m.top.content = m.servers
+    m.log.debug("Jellyfin servers found", m.servers[0], m.servers[1], m.servers[2])
+end sub
+
+sub AddServer(server)
+    if m.serverUrlMap[server.baseUrl] = invalid
+        m.serverUrlMap[server.baseUrl] = true
+        m.servers.push(server)
+    end if
+end sub
+
+sub SendClientDiscoveryBroadcast()
+    m.clientDiscovery = {
+        port: CreateObject("roMessagePort"),
+        address: CreateObject("roSocketAddress"),
+        socket: CreateObject("roDatagramSocket"),
+        urlTransfer: CreateObject("roUrlTransfer")
+    }
+    m.clientDiscovery.address.SetAddress("255.255.255.255:7359")
+    m.clientDiscovery.urlTransfer.SetPort(m.clientDiscoveryPort)
+    m.clientDiscovery.socket.SetMessagePort(m.clientDiscovery.port)
+    m.clientDiscovery.socket.SetSendToAddress(m.clientDiscovery.address)
+    m.clientDiscovery.socket.NotifyReadable(true)
+    m.clientDiscovery.socket.SetBroadcast(true)
+    m.clientDiscovery.socket.SendStr("Who is JellyfinServer?")
+end sub
+
+sub ProcessClientDiscoveryResponse(message)
+    if Type(message) = "roSocketEvent" and message.GetSocketId() = m.clientDiscovery.socket.GetId() and m.clientDiscovery.socket.IsReadable()
+        try
+            responseJson = m.clientDiscovery.socket.ReceiveStr(4096)
+            server = ParseJson(responseJson)
+            AddServer({
+                name: server.Name,
+                baseUrl: server.Address,
+                'hardcoded icon since this service doesn't include them
+                iconUrl: "pkg:/images/logo-icon120.jpg",
+                iconWidth: 120,
+                iconHeight: 120
+            })
+            m.log.info("Found Jellyfin server using client discovery", server.Address)
+        catch e
+            m.log.error("Error scanning for jellyfin server", message)
+        end try
+    end if
+end sub
+
+sub SendSSDPBroadcast()
+    m.ssdp = {
+        port: CreateObject("roMessagePort"),
+        address: CreateObject("roSocketAddress"),
+        socket: CreateObject("roDatagramSocket"),
+        urlTransfer: CreateObject("roUrlTransfer")
+    }
+    m.ssdp.address.SetAddress("239.255.255.250:1900")
+    m.ssdp.socket.SetMessagePort(m.ssdp.port)
+    m.ssdp.socket.SetSendToAddress(m.ssdp.address)
+    m.ssdp.socket.NotifyReadable(true)
+    m.ssdp.urlTransfer.SetPort(m.ssdp.port)
+
+    'brightscript can't escape characters in strings, so create a few vars here so we can use them in the strings below
+    Q = Chr(34)
+    CRLF = Chr(13) + Chr(10)
+
+    ssdpStr = "M-SEARCH * HTTP/1.1" + CRLF
+    ssdpStr += "HOST: 239.255.255.250:1900" + CRLF
+    ssdpStr += "MAN: " + Q + "ssdp:discover" + Q + CRLF
+    ssdpStr += "ST:urn:schemas-upnp-org:device:MediaServer:1" + CRLF
+    ssdpStr += "MX: 2" + CRLF
+    ssdpStr += CRLF
+
+    m.ssdp.socket.SendStr(ssdpStr)
+end sub
+
+sub ProcessSSDPResponse(message)
+    locationUrl = invalid
+    if Type (message) = "roSocketEvent" and message.GetSocketId() = m.ssdp.socket.GetId() and m.ssdp.socket.IsReadable()
+        recvStr = m.ssdp.socket.ReceiveStr(4096)
+        match = CreateObject("roRegex", "\r\nLocation:\s*(.*?)\s*\r\n", "i").Match(recvStr)
+        if match.Count() = 2
+            locationUrl = match[1]
+        end if
+    end if
+
+    if locationUrl = invalid
+        return
+    else if m.locationUrlMap[locationUrl] <> invalid
+        m.log.warn("Already discovered this location", locationUrl)
+        return
+    end if
+
+    m.locationUrlMap[locationUrl] = true
+
+    http = CreateObject("roUrlTransfer")
+    http.SetUrl(locationUrl)
+    responseText = http.GetToString()
+    xml = CreateObject("roXMLElement")
+    'if we successfully parsed the response, process it
+    if xml.Parse(responseText)
+        deviceNode = xml.GetNamedElementsCi("device")[0]
+        manufacturer = deviceNode.GetNamedElementsCi("manufacturer").GetText()
+        'only process jellyfin servers
+        if lcase(manufacturer) = "jellyfin"
+            'find the largest icon
+            width = 0
+            server = invalid
+            icons = deviceNode.GetNamedElementsCi("iconList")[0].GetNamedElementsCi("icon")
+            for each iconNode in icons
+                iconUrl = iconNode.GetNamedElementsCi("url").GetText()
+                baseUrl = invalid
+                match = CreateObject("roRegex", "(.*?)\/dlna\/", "i").Match(iconUrl)
+                if match.Count() = 2
+                    baseUrl = match[1]
+                end if
+                loopResult = {
+                    name: deviceNode.GetNamedElementsCi("friendlyName").GetText(),
+                    baseUrl: baseUrl,
+                    iconUrl: iconUrl,
+                    iconWidth: iconNode.GetNamedElementsCi("width")[0].GetText().ToInt(),
+                    iconHeight: iconNode.GetNamedElementsCi("height")[0].GetText().ToInt()
+                }
+                if baseUrl <> invalid and loopResult.iconWidth > width
+                    width = loopResult.iconWidth
+                    server = loopResult
+                end if
+            end for
+            AddServer(server)
+            m.log.info("Found jellyfin server using SSDP and DLNA", server.baseUrl)
+        end if
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_config_SetServerScreen.bs.html b/docs/api/components_config_SetServerScreen.bs.html new file mode 100644 index 000000000..28fbf33ed --- /dev/null +++ b/docs/api/components_config_SetServerScreen.bs.html @@ -0,0 +1,161 @@ +Source: components/config/SetServerScreen.bs
On this page

components_config_SetServerScreen.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.log = log.Logger("SetServerScreen")
+    m.top.setFocus(true)
+
+    m.serverPicker = m.top.findNode("serverPicker")
+    m.serverUrlTextbox = m.top.findNode("serverUrlTextbox")
+    m.serverUrlContainer = m.top.findNode("serverUrlContainer")
+    m.serverUrlOutline = m.top.findNode("serverUrlOutline")
+    m.submit = m.top.findNode("submit")
+
+    m.top.observeField("serverUrl", "clearErrorMessage")
+
+    ScanForServers()
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    m.log.debug("SetServerScreen onKeyEvent", key, press)
+
+    if not press then return true
+    handled = true
+
+    if key = "OK" and m.serverPicker.hasFocus()
+        m.top.serverUrl = m.serverPicker.content.getChild(m.serverPicker.itemFocused).baseUrl
+        m.submit.setFocus(true)
+        'if the user pressed the down key and we are already at the last child of server picker, then change focus to the url textbox
+    else if key = "down" and m.serverPicker.hasFocus() and m.serverPicker.content.getChildCount() > 0 and m.serverPicker.itemFocused = m.serverPicker.content.getChildCount() - 1
+        m.serverUrlContainer.setFocus(true)
+
+        'user navigating up to the server picker from the input box (it's only focusable if it has items)
+    else if key = "up" and m.serverUrlContainer.hasFocus() and m.servers.Count() > 0
+        m.serverPicker.setFocus(true)
+    else if key = "up" and m.serverUrlContainer.hasFocus() and m.servers.Count() = 0
+        ScanForServers()
+    else if key = "back" and m.serverUrlContainer.hasFocus() and m.servers.Count() > 0
+        m.serverPicker.setFocus(true)
+    else if key = "OK" and m.serverUrlContainer.hasFocus()
+        ShowKeyboard()
+    else if key = "back" and m.submit.hasFocus() and m.servers.Count() > 0
+        m.serverPicker.setFocus(true)
+    else if key = "back" and m.submit.hasFocus() and m.servers.Count() = 0
+        m.serverUrlContainer.setFocus(true)
+    else if key = "back" and m.serverUrlContainer.hasFocus() and m.servers.Count() = 0
+        ScanForServers()
+    else if key = "back" and m.serverPicker.hasFocus() and m.servers.Count() > 0
+        ScanForServers()
+        ' On "back" with or without available local servers, will rescan for servers
+    else if key = "up" and m.submit.hasFocus()
+        m.serverUrlContainer.setFocus(true)
+        'focus the submit button from serverUrl
+    else if key = "down" and m.serverUrlContainer.hasFocus()
+        m.submit.setFocus(true)
+    else if key = "options"
+        if m.serverPicker.itemFocused >= 0 and m.serverPicker.itemFocused < m.serverPicker.content.getChildCount()
+            serverName = m.serverPicker.content.getChild(m.serverPicker.itemFocused).name
+            if m.servers.Count() > 0 and Instr(1, serverName, "Saved") > 0
+                'Can only delete previously saved servers, not locally discovered ones
+                'So if we are on a "Saved" item, let the options dialog be shown (handled elsewhere)
+                handled = false
+            end if
+        end if
+    else
+        handled = false
+    end if
+    'show/hide input box outline
+    m.serverUrlOutline.visible = m.serverUrlContainer.isInFocusChain()
+
+    return handled
+end function
+
+sub ScanForServers()
+    m.ssdpScanner = CreateObject("roSGNode", "ServerDiscoveryTask")
+    'run the task
+    m.ssdpScanner.observeField("content", "ScanForServersComplete")
+    m.ssdpScanner.control = "RUN"
+    startLoadingSpinner(false)
+end sub
+
+sub ScanForServersComplete(event)
+    m.servers = event.getData()
+
+    items = CreateObject("roSGNode", "ContentNode")
+    for each server in m.servers
+        server.subtype = "ContentNode"
+        'add new fields for every server property onto the ContentNode (rather than making a dedicated component just to hold data...)
+        items.update([server], true)
+    end for
+
+    'load any previously logged in to servers as well (if they aren't already discovered on the local network)
+    saved = get_setting("saved_servers")
+    if saved <> invalid
+        savedServers = ParseJson(saved)
+        for each server in savedServers.serverList
+            alreadyListed = false
+            for each listed in m.servers
+                if LCase(listed.baseUrl) = server.baseUrl 'saved server data is always lowercase
+                    alreadyListed = true
+                    exit for
+                end if
+            end for
+            if alreadyListed = false
+                items.update([server], true)
+                m.servers.push(server)
+            end if
+        end for
+    end if
+
+    m.serverPicker.content = items
+    stopLoadingSpinner()
+
+    'if we have at least one server, focus on the server picker
+    if m.servers.Count() > 0
+        m.serverPicker.setFocus(true)
+        'no servers found...focus on the input textbox
+    else
+        m.serverUrlContainer.setFocus(true)
+        'show/hide input box outline
+        m.serverUrlOutline.visible = true
+    end if
+
+end sub
+
+sub ShowKeyboard()
+    dialog = createObject("roSGNode", "StandardKeyboardDialog")
+    dialog.title = tr("Enter the server name or ip address")
+    dialog.buttons = [tr("OK"), tr("Cancel")]
+    dialog.text = m.serverUrlTextbox.text
+    greenPalette = createObject("roSGNode", "RSGPalette")
+    greenPalette.colors = { DialogBackgroundColor: "#2A2B2A" }
+    dialog.palette = greenPalette
+
+    m.top.getscene().dialog = dialog
+    m.dialog = dialog
+
+    dialog.observeField("buttonSelected", "onDialogButton")
+end sub
+
+function onDialogButton()
+    d = m.dialog
+    button_text = d.buttons[d.buttonSelected]
+
+    if button_text = tr("OK")
+        m.serverUrlTextbox.text = d.text
+        m.dialog.close = true
+        return true
+    else if button_text = tr("Cancel")
+        m.dialog.close = true
+        return true
+    else
+        return false
+    end if
+end function
+
+sub clearErrorMessage()
+    m.top.errorMessage = ""
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_AlbumData.bs.html b/docs/api/components_data_AlbumData.bs.html new file mode 100644 index 000000000..a9c01bdf5 --- /dev/null +++ b/docs/api/components_data_AlbumData.bs.html @@ -0,0 +1,10 @@ +Source: components/data/AlbumData.bs
On this page

components_data_AlbumData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+end sub
+
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_ChannelData.bs.html b/docs/api/components_data_ChannelData.bs.html new file mode 100644 index 000000000..4fa16f975 --- /dev/null +++ b/docs/api/components_data_ChannelData.bs.html @@ -0,0 +1,27 @@ +Source: components/data/ChannelData.bs
On this page

components_data_ChannelData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+    m.top.id = json.id
+    m.top.title = json.name
+    m.top.live = true
+    m.top.Type = "TvChannel"
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Primary <> invalid
+        imgParams = { "maxHeight": 60, "Tag": m.top.json.ImageTags.Primary }
+        m.top.hdsmalliconurl = ImageURL(m.top.json.id, "Primary", imgParams)
+
+        imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+        m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_CollectionData.bs.html b/docs/api/components_data_CollectionData.bs.html new file mode 100644 index 000000000..40d361222 --- /dev/null +++ b/docs/api/components_data_CollectionData.bs.html @@ -0,0 +1,43 @@ +Source: components/data/CollectionData.bs
On this page

components_data_CollectionData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.overview = json.overview
+    m.top.Description = json.overview
+    m.top.favorite = json.UserData.isFavorite
+    m.top.watched = json.UserData.played
+    m.top.Type = "Boxset"
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        if m.top.json.ImageTags.Primary <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags <> invalid
+            imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_FolderData.bs.html b/docs/api/components_data_FolderData.bs.html new file mode 100644 index 000000000..acb42e0cd --- /dev/null +++ b/docs/api/components_data_FolderData.bs.html @@ -0,0 +1,36 @@ +Source: components/data/FolderData.bs
On this page

components_data_FolderData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Type = "Folder"
+
+    m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png"
+    ' This is a temporary measure to avoid displaying landscape photos
+    ' in GridItem components that only support portrait. It will be fixed
+    ' after the ItemGrid is reworked.
+    if m.top.json.Type <> "CollectionFolder"
+        setPoster()
+    end if
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else if m.top.json.Type = "Studio"
+        imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
+        m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
+    else if m.top.json.ImageTags.Primary <> invalid
+        imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+        m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+    end if
+end sub
+
+'TODO Set network Poster image
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_GetFiltersTask.bs.html b/docs/api/components_data_GetFiltersTask.bs.html new file mode 100644 index 000000000..523c622a9 --- /dev/null +++ b/docs/api/components_data_GetFiltersTask.bs.html @@ -0,0 +1,14 @@ +Source: components/data/GetFiltersTask.bs
On this page

components_data_GetFiltersTask.bs

import "pkg:/source/utils/config.bs"
+import "pkg:/source/api/sdk.bs"
+
+sub init()
+    m.top.functionName = "getFiltersTask"
+end sub
+
+sub getFiltersTask()
+    m.filters = api.items.GetFilters(m.top.params)
+    m.top.filters = m.filters
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_HomeData.bs.html b/docs/api/components_data_HomeData.bs.html new file mode 100644 index 000000000..0e618fd2b --- /dev/null +++ b/docs/api/components_data_HomeData.bs.html @@ -0,0 +1,139 @@ +Source: components/data/HomeData.bs
On this page

components_data_HomeData.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/api/Image.bs"
+
+sub setData()
+    ' We keep json around just as a reference,
+    ' but ideally everything should be going through one of the interfaces
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.name = datum.name
+    m.top.type = datum.type
+
+    if datum.CollectionType = invalid
+        m.top.CollectionType = datum.type
+    else
+        m.top.CollectionType = datum.CollectionType
+    end if
+
+    ' Set appropriate Images for Wide and Tall based on type
+
+    if datum.type = "CollectionFolder" or datum.type = "UserView"
+        params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
+        m.top.widePosterUrl = m.top.thumbnailURL
+
+        ' Add Icon URLs for display if there is no Poster
+        if datum.CollectionType = "livetv"
+            m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
+        else if datum.CollectionType = "folders"
+            m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png"
+        end if
+
+    else if datum.type = "Episode" or datum.type = "MusicVideo"
+        m.top.isWatched = datum.UserData.Played
+
+        imgParams = {}
+        imgParams.Append({ "maxHeight": 261 })
+        imgParams.Append({ "maxWidth": 464 })
+
+        if datum.ImageTags.Primary <> invalid
+            param = { "Tag": datum.ImageTags.Primary }
+            imgParams.Append(param)
+        end if
+
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", imgParams)
+
+        ' Add Wide Poster  (Series Backdrop)
+        if datum.ParentThumbImageTag <> invalid
+            imgParams["Tag"] = datum.ParentThumbImageTag
+            m.top.widePosterUrl = ImageURL(datum.ParentThumbItemId, "Thumb", imgParams)
+        else if datum.ParentBackdropImageTags <> invalid
+            imgParams["Tag"] = datum.ParentBackdropImageTags[0]
+            m.top.widePosterUrl = ImageURL(datum.ParentBackdropItemId, "Backdrop", imgParams)
+        else if datum.ImageTags.Primary <> invalid
+            imgParams["Tag"] = datum.SeriesPrimaryImageTag
+            m.top.widePosterUrl = ImageURL(datum.id, "Primary", imgParams)
+        end if
+
+    else if datum.type = "Series"
+        imgParams = { "maxHeight": 261 }
+        imgParams.Append({ "maxWidth": 464 })
+
+        m.top.posterURL = ImageURL(datum.id, "Primary", imgParams)
+
+        ' Add Wide Poster  (Series Backdrop)
+        if datum.ImageTags <> invalid and datum.imageTags.Thumb <> invalid
+            imgParams["Tag"] = datum.imageTags.Thumb
+            m.top.widePosterUrl = ImageURL(datum.Id, "Thumb", imgParams)
+        else if datum.BackdropImageTags <> invalid
+            imgParams["Tag"] = datum.BackdropImageTags[0]
+            m.top.widePosterUrl = ImageURL(datum.Id, "Backdrop", imgParams)
+        end if
+
+    else if datum.type = "Movie"
+        m.top.isWatched = datum.UserData.Played
+
+        imgParams = {}
+        imgParams.Append({ "maxHeight": 261 })
+        imgParams.Append({ "maxWidth": 175 })
+
+        if datum.ImageTags.Primary <> invalid
+            param = { "Tag": datum.ImageTags.Primary }
+            imgParams.Append(param)
+        end if
+
+        m.top.posterURL = ImageURL(datum.id, "Primary", imgParams)
+
+        ' For wide image, use backdrop
+        imgParams["maxWidth"] = 464
+
+        if datum.ImageTags <> invalid and datum.imageTags.Thumb <> invalid
+            imgParams["Tag"] = datum.imageTags.Thumb
+            m.top.thumbnailUrl = ImageURL(datum.Id, "Thumb", imgParams)
+        else if datum.BackdropImageTags[0] <> invalid
+            imgParams["Tag"] = datum.BackdropImageTags[0]
+            m.top.thumbnailUrl = ImageURL(datum.id, "Backdrop", imgParams)
+        end if
+    else if datum.type = "Video"
+        m.top.isWatched = datum.UserData.Played
+
+        imgParams = {
+            "maxHeight": 261,
+            "maxWidth": 464
+        }
+
+        if datum.ImageTags <> invalid and datum.ImageTags.Primary <> invalid
+            imgParams.Append({ "Tag": datum.ImageTags.Primary })
+        end if
+
+        m.top.posterURL = ImageURL(datum.id, "Primary", imgParams)
+        m.top.thumbnailUrl = m.top.posterURL
+    else if datum.type = "MusicAlbum"
+        params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 261 }
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
+        m.top.widePosterUrl = m.top.thumbnailURL
+        m.top.posterUrl = m.top.thumbnailURL
+    else if datum.type = "TvChannel" or datum.type = "Channel"
+        params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
+        m.top.widePosterUrl = m.top.thumbnailURL
+        m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
+    else if datum.type = "Photo"
+        params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
+
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
+        m.top.widePosterUrl = m.top.thumbnailURL
+        m.top.posterUrl = m.top.thumbnailURL
+    else if datum.type = "PhotoAlbum"
+        params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
+
+        m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
+        m.top.widePosterUrl = m.top.thumbnailURL
+        m.top.posterUrl = m.top.thumbnailURL
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_ImageData.bs.html b/docs/api/components_data_ImageData.bs.html new file mode 100644 index 000000000..b8fe9093f --- /dev/null +++ b/docs/api/components_data_ImageData.bs.html @@ -0,0 +1,10 @@ +Source: components/data/ImageData.bs
On this page

components_data_ImageData.bs

sub setFields()
+    json = m.top.json
+    m.top.imagetype = json.imagetype
+    m.top.size = json.size
+    m.top.height = json.height
+    m.top.width = json.width
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_MovieData.bs.html b/docs/api/components_data_MovieData.bs.html new file mode 100644 index 000000000..a2bf6b154 --- /dev/null +++ b/docs/api/components_data_MovieData.bs.html @@ -0,0 +1,83 @@ +Source: components/data/MovieData.bs
On this page

components_data_MovieData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/misc.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Description = json.overview
+    m.top.favorite = json.UserData.isFavorite
+    m.top.watched = json.UserData.played
+    m.top.Type = "Movie"
+
+    if isValid(json.MediaSourceCount) and json.MediaSourceCount > 1
+        if isValid(json.MediaSources)
+            m.top.mediaSources = []
+            for each source in json.MediaSources
+                m.top.mediaSources.push(source)
+            end for
+        end if
+    end if
+
+    if json.ProductionYear <> invalid
+        m.top.SubTitle = json.ProductionYear
+    end if
+
+    if json.OfficialRating <> invalid and json.OfficialRating <> ""
+        m.top.Rating = json.OfficialRating
+        if m.top.SubTitle <> ""
+            m.top.SubTitle = m.top.SubTitle + " - " + m.top.Rating
+        else
+            m.top.SubTitle = m.top.Rating
+        end if
+    end if
+
+    setPoster()
+    setContainer()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        if isValid(m.top.json)
+            if isValid(m.top.json.ImageTags) and isValid(m.top.json.ImageTags.Primary)
+                imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+                m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+            else if isValid(m.top.json.BackdropImageTags) and isValid(m.top.json.BackdropImageTags[0])
+                imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
+                m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+            else if isValid(m.top.json.ParentThumbImageTag) and isValid(m.top.json.ParentThumbItemId)
+                imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
+                m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
+            end if
+
+            ' Add Backdrop Image
+            if isValid(m.top.json.BackdropImageTags) and isValid(m.top.json.BackdropImageTags[0])
+                imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
+                m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+            end if
+        end if
+    end if
+end sub
+
+sub setContainer()
+    json = m.top.json
+
+    if json.mediaSources = invalid then return
+    if json.mediaSources.count() = 0 then return
+
+    m.top.container = json.mediaSources[0].container
+
+    if m.top.container = invalid then m.top.container = ""
+
+    if m.top.container = "m4v" or m.top.container = "mov"
+        m.top.container = "mp4"
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_MusicAlbumData.bs.html b/docs/api/components_data_MusicAlbumData.bs.html new file mode 100644 index 000000000..0fcf048a0 --- /dev/null +++ b/docs/api/components_data_MusicAlbumData.bs.html @@ -0,0 +1,18 @@ +Source: components/data/MusicAlbumData.bs
On this page

components_data_MusicAlbumData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.overview = datum.overview
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_MusicAlbumSongListData.bs.html b/docs/api/components_data_MusicAlbumSongListData.bs.html new file mode 100644 index 000000000..702a84814 --- /dev/null +++ b/docs/api/components_data_MusicAlbumSongListData.bs.html @@ -0,0 +1,39 @@ +Source: components/data/MusicAlbumSongListData.bs
On this page

components_data_MusicAlbumSongListData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+    m.top.id = json.id
+    m.top.favorite = json.UserData.isFavorite
+    m.top.Type = "MusicAlbum"
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        if m.top.json.ImageTags.Primary <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295 }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 440 }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295 }
+            m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280 }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_MusicArtistData.bs.html b/docs/api/components_data_MusicArtistData.bs.html new file mode 100644 index 000000000..8d183814b --- /dev/null +++ b/docs/api/components_data_MusicArtistData.bs.html @@ -0,0 +1,42 @@ +Source: components/data/MusicArtistData.bs
On this page

components_data_MusicArtistData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+    m.top.id = json.id
+    m.top.favorite = json.UserData.isFavorite
+    m.top.Type = "MusicArtist"
+    setPoster()
+
+    m.top.title = json.name
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        ' Add Artist Image
+        if m.top.json.ImageTags.Primary <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 440 }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 440 }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 440 }
+            m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280 }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_MusicSongData.bs.html b/docs/api/components_data_MusicSongData.bs.html new file mode 100644 index 000000000..d3916185a --- /dev/null +++ b/docs/api/components_data_MusicSongData.bs.html @@ -0,0 +1,20 @@ +Source: components/data/MusicSongData.bs
On this page

components_data_MusicSongData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.overview = datum.overview
+    m.top.trackNumber = datum.IndexNumber
+    m.top.length = datum.RunTimeTicks
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_OptionsButton.bs.html b/docs/api/components_data_OptionsButton.bs.html new file mode 100644 index 000000000..bece1b12b --- /dev/null +++ b/docs/api/components_data_OptionsButton.bs.html @@ -0,0 +1,10 @@ +Source: components/data/OptionsButton.bs
On this page

components_data_OptionsButton.bs

import "pkg:/source/utils/config.bs"
+
+sub init()
+end sub
+
+sub press()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_OptionsData.bs.html b/docs/api/components_data_OptionsData.bs.html new file mode 100644 index 000000000..4f70a6bb6 --- /dev/null +++ b/docs/api/components_data_OptionsData.bs.html @@ -0,0 +1,43 @@ +Source: components/data/OptionsData.bs
On this page

components_data_OptionsData.bs

import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.value_index = 0
+end sub
+
+sub update_title()
+    if m.top.choices.count() = 0
+        m.top.title = m.top.base_title + ": <none>"
+        return
+    end if
+
+    for i = 0 to m.top.choices.count() - 1
+        if m.top.choices[i].value = m.top.value
+            m.top.value_index = i
+            exit for
+        end if
+    end for
+    m.top.title = m.top.base_title + ": " + m.top.choices[m.top.value_index].display
+end sub
+
+sub press()
+    max_opt = m.top.choices.count()
+    i = m.top.value_index + 1
+    while i >= max_opt
+        i = i - max_opt
+    end while
+
+    m.top.value_index = i
+    m.top.value = m.top.choices[m.top.value_index].value
+
+    if m.top.config_key = "" or m.top.config_key = invalid
+        return
+    end if
+    if m.top.global_setting
+        set_setting(m.top.config_key, m.top.value)
+    else
+        set_user_setting(m.top.config_key, m.top.value)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_PersonData.bs.html b/docs/api/components_data_PersonData.bs.html new file mode 100644 index 000000000..32872aadf --- /dev/null +++ b/docs/api/components_data_PersonData.bs.html @@ -0,0 +1,39 @@ +Source: components/data/PersonData.bs
On this page

components_data_PersonData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+    m.top.id = json.id
+    m.top.favorite = json.UserData.isFavorite
+    m.top.Type = "Person"
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        if m.top.json.ImageTags.Primary <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
+            m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_PhotoData.bs.html b/docs/api/components_data_PhotoData.bs.html new file mode 100644 index 000000000..f8bf403f2 --- /dev/null +++ b/docs/api/components_data_PhotoData.bs.html @@ -0,0 +1,41 @@ +Source: components/data/PhotoData.bs
On this page

components_data_PhotoData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Type = "Photo"
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        if m.top.json.ImageTags.Primary <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
+            m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags[0] <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_PlaylistData.bs.html b/docs/api/components_data_PlaylistData.bs.html new file mode 100644 index 000000000..2bcbbaf8c --- /dev/null +++ b/docs/api/components_data_PlaylistData.bs.html @@ -0,0 +1,18 @@ +Source: components/data/PlaylistData.bs
On this page

components_data_PlaylistData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.overview = datum.overview
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_PublicUserData.bs.html b/docs/api/components_data_PublicUserData.bs.html new file mode 100644 index 000000000..88b931b41 --- /dev/null +++ b/docs/api/components_data_PublicUserData.bs.html @@ -0,0 +1,5 @@ +Source: components/data/PublicUserData.bs
On this page

components_data_PublicUserData.bs

sub init()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_SceneManager.bs.html b/docs/api/components_data_SceneManager.bs.html new file mode 100644 index 000000000..066ec948a --- /dev/null +++ b/docs/api/components_data_SceneManager.bs.html @@ -0,0 +1,358 @@ +Source: components/data/SceneManager.bs
On this page

components_data_SceneManager.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"
+import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.log = log.Logger("SceneManager")
+    m.groups = []
+    m.scene = m.top.getScene()
+    m.content = m.scene.findNode("content")
+    m.overhang = m.scene.findNode("overhang")
+end sub
+
+'
+' Push a new group onto the stack, replacing the existing group on the screen
+sub pushScene(newGroup)
+
+    currentGroup = m.groups.peek()
+    if newGroup <> invalid
+        if currentGroup <> invalid
+            'Search through group and store off last focused item
+            if currentGroup.focusedChild <> invalid
+                focused = currentGroup.focusedChild
+                while focused.hasFocus() = false
+                    focused = focused.focusedChild
+                end while
+
+                currentGroup.lastFocus = focused
+                currentGroup.setFocus(false)
+            else
+                currentGroup.setFocus(false)
+            end if
+
+            if currentGroup.isSubType("JFGroup")
+                unregisterOverhangData(currentGroup)
+            end if
+
+            currentGroup.visible = false
+
+            if currentGroup.isSubType("JFScreen")
+                currentGroup.callFunc("OnScreenHidden")
+            end if
+
+        end if
+
+        m.groups.push(newGroup)
+
+        if currentGroup <> invalid
+            m.content.replaceChild(newGroup, 0)
+        else
+            m.content.appendChild(newGroup)
+        end if
+
+        if newGroup.isSubType("JFScreen")
+            newGroup.callFunc("OnScreenShown")
+        end if
+
+        'observe info about new group, set overhang title, etc.
+        if newGroup.isSubType("JFGroup")
+            registerOverhangData(newGroup)
+
+            ' Some groups set focus to a specific component within init(), so we don't want to
+            ' change if that is the case.
+            if newGroup.isInFocusChain() = false
+                newGroup.setFocus(true)
+            end if
+        else if newGroup.isSubType("JFVideo")
+            newGroup.setFocus(true)
+            newGroup.control = "play"
+            m.overhang.visible = false
+        end if
+    else
+        currentGroup.focusedChild.setFocus(true)
+    end if
+
+end sub
+
+'
+' Remove the current group and load the last group from the stack
+sub popScene()
+    group = m.groups.pop()
+    if group <> invalid
+        if group.isSubType("JFGroup")
+            unregisterOverhangData(group)
+        else if group.isSubType("JFVideo")
+            ' Stop video to make sure app communicates stop playstate to server
+            group.control = "stop"
+        end if
+
+        group.visible = false
+
+        if group.isSubType("JFScreen")
+            group.callFunc("OnScreenHidden")
+        end if
+    else
+        ' Exit app if for some reason we don't have anything on the stack
+        m.scene.exit = true
+    end if
+
+    group = m.groups.peek()
+    if group <> invalid
+        registerOverhangData(group)
+
+        group.visible = true
+
+        m.content.replaceChild(group, 0)
+
+        if group.isSubType("JFScreen")
+            group.callFunc("OnScreenShown")
+        else
+            ' Restore focus
+            if group.lastFocus <> invalid
+                group.lastFocus.setFocus(true)
+            else
+                if group.focusedChild <> invalid
+                    group.focusedChild.setFocus(true)
+                else
+                    group.setFocus(true)
+                end if
+            end if
+        end if
+    else
+        ' Exit app if the stack is empty after removing group
+        m.scene.exit = true
+    end if
+    stopLoadingSpinner()
+end sub
+
+
+'
+' Return group at top of stack without removing
+function getActiveScene() as object
+    return m.groups.peek()
+end function
+
+
+'
+' Clear all content from group stack
+sub clearScenes()
+    if m.content <> invalid then m.content.removeChildrenIndex(m.content.getChildCount(), 0)
+    for each group in m.groups
+        if LCase(group.subtype()) = "jfscreen"
+            group.callFunc("OnScreenHidden")
+        end if
+    end for
+    m.groups = []
+end sub
+
+'
+' Clear previous scene from group stack
+sub clearPreviousScene()
+    m.groups.pop()
+end sub
+
+'
+' Delete scene from group stack at passed index
+sub deleteSceneAtIndex(index = 1)
+    m.groups.Delete(index)
+end sub
+
+'
+' Display user/device settings screen
+sub settings()
+    settingsScreen = createObject("roSGNode", "Settings")
+    pushScene(settingsScreen)
+end sub
+
+'
+' Register observers for overhang data
+sub registerOverhangData(group)
+    if group.isSubType("JFGroup")
+        if group.overhangTitle <> invalid then m.overhang.title = group.overhangTitle
+
+        if group.optionsAvailable
+            m.overhang.showOptions = true
+        else
+            m.overhang.showOptions = false
+        end if
+        group.observeField("optionsAvailable", "updateOptions")
+
+        group.observeField("overhangTitle", "updateOverhangTitle")
+
+        if group.overhangVisible
+            m.overhang.visible = true
+        else
+            m.overhang.visible = false
+        end if
+        group.observeField("overhangVisible", "updateOverhangVisible")
+    else if group.isSubType("JFVideo")
+        m.overhang.visible = false
+    else
+        m.log.error("registerOverhangData(): Unexpected group type.", group, group.subtype())
+    end if
+end sub
+
+
+'
+' Remove observers for overhang data
+sub unregisterOverhangData(group)
+    group.unobserveField("overhangTitle")
+end sub
+
+
+'
+' Update overhang title
+sub updateOverhangTitle(msg)
+    m.overhang.title = msg.getData()
+end sub
+
+
+'
+' Update options availability
+sub updateOptions(msg)
+    m.overhang.showOptions = msg.getData()
+end sub
+
+
+'
+' Update whether the overhang is visible or not
+sub updateOverhangVisible(msg)
+    m.overhang.visible = msg.getData()
+end sub
+
+
+'
+' Update username in overhang
+sub updateUser()
+    ' Passthrough to overhang
+    if m.overhang <> invalid then m.overhang.currentUser = m.top.currentUser
+end sub
+
+
+'
+' Reset time
+sub resetTime()
+    ' Passthrough to overhang
+    m.overhang.callFunc("resetTime")
+end sub
+
+'
+' Display dialog to user with an OK button
+sub userMessage(title as string, message as string)
+    dialog = createObject("roSGNode", "StandardMessageDialog")
+    dialog.title = title
+    dialog.message = message
+    dialog.buttons = [tr("OK")]
+    dialog.observeField("buttonSelected", "dismissDialog")
+    m.scene.dialog = dialog
+end sub
+
+'
+' Display dialog to user with an OK button
+sub standardDialog(title, message)
+    dialog = createObject("roSGNode", "StandardDialog")
+    dlgPalette = createObject("roSGNode", "RSGPalette")
+    dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "#00a4dcFF",
+        DialogTextColor: "0xeeeeeeFF"
+    }
+    dialog.palette = dlgPalette
+    dialog.observeField("buttonSelected", "dismissDialog")
+    dialog.title = title
+    dialog.contentData = message
+    dialog.buttons = [tr("OK")]
+
+    m.scene.dialog = dialog
+end sub
+
+'
+' Display dialog to user with an OK button
+sub radioDialog(title, message)
+    dialog = createObject("roSGNode", "RadioDialog")
+    dlgPalette = createObject("roSGNode", "RSGPalette")
+    dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "#00a4dcFF",
+        DialogTextColor: "0xeeeeeeFF"
+    }
+    dialog.palette = dlgPalette
+    dialog.observeField("buttonSelected", "dismissDialog")
+    dialog.title = title
+    dialog.contentData = message
+    dialog.buttons = [tr("OK")]
+
+    m.scene.dialog = dialog
+end sub
+
+'
+' Display dialog to user with an OK button
+sub optionDialog(title, message, buttons)
+    m.top.dataReturned = false
+    m.top.returnData = invalid
+    m.userselection = false
+
+    dialog = createObject("roSGNode", "StandardMessageDialog")
+    dlgPalette = createObject("roSGNode", "RSGPalette")
+    dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "#00a4dcFF",
+        DialogTextColor: "0xeeeeeeFF"
+    }
+    dialog.palette = dlgPalette
+    dialog.observeField("buttonSelected", "optionSelected")
+    dialog.observeField("wasClosed", "optionClosed")
+    dialog.title = title
+    dialog.message = message
+    dialog.buttons = buttons
+
+    m.scene.dialog = dialog
+end sub
+
+'
+' Return button the user selected
+sub optionClosed()
+    if m.userselection then return
+
+    m.top.returnData = {
+        indexSelected: -1,
+        buttonSelected: ""
+    }
+    m.top.dataReturned = true
+end sub
+
+'
+' Return button the user selected
+sub optionSelected()
+    m.userselection = true
+    m.top.returnData = {
+        indexSelected: m.scene.dialog.buttonSelected,
+        buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected]
+    }
+    m.top.dataReturned = true
+
+    dismissDialog()
+end sub
+
+'
+' Close currently displayed dialog
+sub dismissDialog()
+    m.scene.dialog.close = true
+end sub
+
+'
+' Returns bool indicating if dialog is currently displayed
+function isDialogOpen() as boolean
+    return m.scene.dialog <> invalid
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_ScheduleProgramData.bs.html b/docs/api/components_data_ScheduleProgramData.bs.html new file mode 100644 index 000000000..9c00c482e --- /dev/null +++ b/docs/api/components_data_ScheduleProgramData.bs.html @@ -0,0 +1,50 @@ +Source: components/data/ScheduleProgramData.bs
On this page

components_data_ScheduleProgramData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    startDate = createObject("roDateTime")
+    endDate = createObject("roDateTime")
+    startDate.FromISO8601String(json.StartDate)
+    endDate.FromISO8601String(json.EndDate)
+
+    m.top.Title = json.Name
+    m.top.PlayStart = startDate.AsSeconds()
+    m.top.PlayDuration = endDate.AsSeconds() - m.top.PlayStart
+    m.top.Id = json.Id
+    m.top.Description = json.overview
+    m.top.EpisodeTitle = json.EpisodeTitle
+    m.top.isLive = json.isLive
+    m.top.isRepeat = json.isRepeat
+    m.top.startDate = json.startDate
+    m.top.endDate = json.endDate
+    m.top.channelId = json.channelId
+
+    if json.IsSeries <> invalid and json.IsSeries = true
+        if json.IndexNumber <> invalid
+            m.top.episodeNumber = json.IndexNumber
+        end if
+
+        if json.ParentIndexNumber <> invalid
+            m.top.seasonNumber = json.ParentIndexNumber
+        end if
+    end if
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Thumb <> invalid
+            imgParams = { "maxHeight": 500, "maxWidth": 500, "Tag": m.top.json.ImageTags.Thumb }
+            m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
+        end if
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_SearchData.bs.html b/docs/api/components_data_SearchData.bs.html new file mode 100644 index 000000000..a6946d794 --- /dev/null +++ b/docs/api/components_data_SearchData.bs.html @@ -0,0 +1,18 @@ +Source: components/data/SearchData.bs
On this page

components_data_SearchData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.type = datum.Type
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_SeriesData.bs.html b/docs/api/components_data_SeriesData.bs.html new file mode 100644 index 000000000..f805d56bc --- /dev/null +++ b/docs/api/components_data_SeriesData.bs.html @@ -0,0 +1,56 @@ +Source: components/data/SeriesData.bs
On this page

components_data_SeriesData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Description = json.overview
+    m.top.favorite = json.UserData.isFavorite
+    m.top.watched = json.UserData.played
+    m.top.Type = "Series"
+    m.top.overview = json.overview
+
+    if json.ProductionYear <> invalid
+        m.top.SubTitle = json.ProductionYear
+    end if
+
+    if json.OfficialRating <> invalid and json.OfficialRating <> ""
+        m.top.Rating = json.OfficialRating
+        if m.top.SubTitle <> ""
+            m.top.SubTitle = m.top.SubTitle + " - " + m.top.Rating
+        else
+            m.top.SubTitle = m.top.Rating
+        end if
+    end if
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+
+        if m.top.json.ImageTags.Primary <> invalid
+
+            imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+            m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+        else if m.top.json.BackdropImageTags <> invalid
+            imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+        ' Add Backdrop Image
+        if m.top.json.BackdropImageTags <> invalid
+            imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
+            m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
+        end if
+
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_TVEpisode.bs.html b/docs/api/components_data_TVEpisode.bs.html new file mode 100644 index 000000000..df9532496 --- /dev/null +++ b/docs/api/components_data_TVEpisode.bs.html @@ -0,0 +1,28 @@ +Source: components/data/TVEpisode.bs
On this page

components_data_TVEpisode.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Description = json.overview
+    m.top.favorite = json.UserData.isFavorite
+    m.top.watched = json.UserData.played
+    m.top.Type = json.Type
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else if m.top.json.ImageTags.Primary <> invalid
+        imgParams = { "maxHeight": 440, "maxWidth": 295 }
+        m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_TVEpisodeData.bs.html b/docs/api/components_data_TVEpisodeData.bs.html new file mode 100644 index 000000000..88ba80449 --- /dev/null +++ b/docs/api/components_data_TVEpisodeData.bs.html @@ -0,0 +1,21 @@ +Source: components/data/TVEpisodeData.bs
On this page

components_data_TVEpisodeData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.showID = datum.SeriesID
+    m.top.seasonID = datum.SeasonID
+    m.top.overview = datum.overview
+    m.top.favorite = datum.UserData.isFavorite
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_TVSeasonData.bs.html b/docs/api/components_data_TVSeasonData.bs.html new file mode 100644 index 000000000..3bac9a9ea --- /dev/null +++ b/docs/api/components_data_TVSeasonData.bs.html @@ -0,0 +1,21 @@ +Source: components/data/TVSeasonData.bs
On this page

components_data_TVSeasonData.bs

sub setFields()
+    datum = m.top.json
+
+    m.top.id = datum.id
+    m.top.title = datum.name
+    m.top.overview = datum.overview
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else
+        m.top.posterURL = ""
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_UserData.bs.html b/docs/api/components_data_UserData.bs.html new file mode 100644 index 000000000..401238771 --- /dev/null +++ b/docs/api/components_data_UserData.bs.html @@ -0,0 +1,61 @@ +Source: components/data/UserData.bs
On this page

components_data_UserData.bs

import "pkg:/source/utils/config.bs"
+
+sub setDataFromJSON()
+    json = m.top.json
+    loadFromJSON(json)
+end sub
+
+sub loadFromJSON(json)
+    m.top.id = json.User.id
+
+    m.top.username = json.User.name
+    m.top.token = json.AccessToken
+end sub
+
+sub loadFromRegistry(id as string)
+    m.top.id = id
+
+    m.top.username = get_user_setting("username")
+    m.top.token = get_user_setting("token")
+end sub
+
+sub saveToRegistry()
+    users = parseJson(get_setting("available_users", "[]"))
+    this_user = invalid
+    for each user in users
+        if user.id = m.top.id then this_user = user
+    end for
+    if this_user = invalid
+        users.push({
+            id: m.top.id,
+            username: m.top.username,
+            server: m.global.session.server.url
+        })
+        set_setting("available_users", formatJson(users))
+    end if
+end sub
+
+sub removeFromRegistry()
+    new_users = []
+    users = parseJson(get_setting("available_users", "[]"))
+    for each user in users
+        if m.top.id <> user.id then new_users.push(user)
+    end for
+
+    set_setting("available_users", formatJson(new_users))
+end sub
+
+function getPreference(key as string)
+    return get_user_setting("pref-" + key)
+end function
+
+function setPreference(key as string, value as string)
+    return set_user_setting("pref-" + key, value)
+end function
+
+sub setServer(hostname as string)
+    m.top.server = hostname
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_data_VideoData.bs.html b/docs/api/components_data_VideoData.bs.html new file mode 100644 index 000000000..c5abc23ec --- /dev/null +++ b/docs/api/components_data_VideoData.bs.html @@ -0,0 +1,28 @@ +Source: components/data/VideoData.bs
On this page

components_data_VideoData.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub setFields()
+    json = m.top.json
+
+    m.top.id = json.id
+    m.top.Title = json.name
+    m.top.Description = json.overview
+    m.top.favorite = json.UserData.isFavorite
+    m.top.watched = json.UserData.played
+    m.top.Type = "Video"
+
+    setPoster()
+end sub
+
+sub setPoster()
+    if m.top.image <> invalid
+        m.top.posterURL = m.top.image.url
+    else if m.top.json.ImageTags.Primary <> invalid
+        imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
+        m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_extras_ExtrasItem.bs.html b/docs/api/components_extras_ExtrasItem.bs.html new file mode 100644 index 000000000..e0d1075bb --- /dev/null +++ b/docs/api/components_extras_ExtrasItem.bs.html @@ -0,0 +1,40 @@ +Source: components/extras/ExtrasItem.bs
On this page

components_extras_ExtrasItem.bs

sub init()
+    m.posterImg = m.top.findNode("posterImg")
+    m.name = m.top.findNode("pLabel")
+    m.role = m.top.findNode("subTitle")
+end sub
+
+sub showContent()
+    if m.top.itemContent <> invalid
+        cont = m.top.itemContent
+        m.name.text = cont.labelText
+        m.name.maxWidth = cont.imageWidth
+        m.role.maxWidth = cont.imageWidth
+        m.posterImg.uri = cont.posterUrl
+        m.posterImg.width = cont.imageWidth
+        m.role.Text = cont.subTitle
+    else
+        m.role.text = tr("Unknown")
+        m.posterImg.uri = "pkg:/images/baseline_person_white_48dp.png"
+    end if
+end sub
+
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        m.name.repeatCount = -1
+        m.role.repeatCount = -1
+    else
+        m.name.repeatCount = 0
+        m.role.repeatCount = 0
+    end if
+
+    if m.global.device.isAudioGuideEnabled = true
+        txt2Speech = CreateObject("roTextToSpeech")
+        txt2Speech.Flush()
+        txt2Speech.Say(m.name.text)
+        txt2Speech.Say(m.role.text)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_extras_ExtrasRowList.bs.html b/docs/api/components_extras_ExtrasRowList.bs.html new file mode 100644 index 000000000..7f98d3adb --- /dev/null +++ b/docs/api/components_extras_ExtrasRowList.bs.html @@ -0,0 +1,217 @@ +Source: components/extras/ExtrasRowList.bs
On this page

components_extras_ExtrasRowList.bs

sub init()
+    m.top.visible = true
+    updateSize()
+    m.top.rowFocusAnimationStyle = "fixedFocus"
+    m.top.observeField("rowItemSelected", "onRowItemSelected")
+    m.top.observeField("rowItemFocused", "onRowItemFocused")
+
+    ' Set up all Tasks
+    m.LoadPeopleTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadPeopleTask.itemsToLoad = "people"
+    m.LoadPeopleTask.observeField("content", "onPeopleLoaded")
+    m.LikeThisTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LikeThisTask.itemsToLoad = "likethis"
+    m.LikeThisTask.observeField("content", "onLikeThisLoaded")
+    m.SpecialFeaturesTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.SpecialFeaturesTask.itemsToLoad = "specialfeatures"
+    m.SpecialFeaturesTask.observeField("content", "onSpecialFeaturesLoaded")
+    m.LoadAdditionalPartsTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadAdditionalPartsTask.itemsToLoad = "additionalparts"
+    m.LoadAdditionalPartsTask.observeField("content", "onAdditionalPartsLoaded")
+    m.LoadMoviesTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadMoviesTask.itemsToLoad = "personMovies"
+    m.LoadShowsTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadShowsTask.itemsToLoad = "personTVShows"
+    m.LoadSeriesTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadSeriesTask.itemsToLoad = "personSeries"
+end sub
+
+sub updateSize()
+    itemHeight = 396
+    m.top.itemSize = [1710, itemHeight]
+    m.top.rowItemSpacing = [36, 36]
+end sub
+
+sub loadParts(data as object)
+    m.top.parentId = data.id
+    m.people = data.People
+    m.LoadAdditionalPartsTask.itemId = m.top.parentId
+    m.LoadAdditionalPartsTask.control = "RUN"
+end sub
+
+sub loadPersonVideos(personId)
+    m.personId = personId
+    m.LoadMoviesTask.itemId = m.personId
+    m.LoadMoviesTask.observeField("content", "onMoviesLoaded")
+    m.LoadMoviesTask.control = "RUN"
+end sub
+
+sub onAdditionalPartsLoaded()
+    parts = m.LoadAdditionalPartsTask.content
+    m.LoadAdditionalPartsTask.unobserveField("content")
+
+    data = CreateObject("roSGNode", "ContentNode") ' The row Node
+    m.top.content = data
+    if parts <> invalid and parts.count() > 0
+        row = buildRow("Additional Parts", parts, 464)
+        addRowSize([464, 291])
+        m.top.content.appendChild(row)
+        m.top.rowItemSize = [[464, 291]]
+    else
+        m.top.rowItemSize = [[234, 396]]
+    end if
+
+    ' Load Cast and Crew and everything else...
+    m.LoadPeopleTask.peopleList = m.people
+    m.LoadPeopleTask.control = "RUN"
+end sub
+
+sub onPeopleLoaded()
+    people = m.LoadPeopleTask.content
+    m.loadPeopleTask.unobserveField("content")
+    if people <> invalid and people.count() > 0
+        row = m.top.content.createChild("ContentNode")
+        row.Title = tr("Cast & Crew")
+        for each person in people
+            if person.json.type = "Actor" and person.json.Role <> invalid and person.json.Role.ToStr().Trim() <> ""
+                person.subTitle = "as " + person.json.Role
+            else
+                person.subTitle = person.json.Type
+            end if
+            person.Type = "Person"
+            row.appendChild(person)
+        end for
+    end if
+    m.LikeThisTask.itemId = m.top.parentId
+    m.LikeThisTask.control = "RUN"
+end sub
+
+sub onLikeThisLoaded()
+    data = m.LikeThisTask.content
+    m.LikeThisTask.unobserveField("content")
+    if data <> invalid and data.count() > 0
+        row = m.top.content.createChild("ContentNode")
+        row.Title = tr("More Like This")
+        for each item in data
+            item.Id = item.json.Id
+            item.labelText = item.json.Name
+            if item.json.ProductionYear <> invalid
+                item.subTitle = stri(item.json.ProductionYear)
+            else if item.json.PremiereDate <> invalid
+                premierYear = CreateObject("roDateTime")
+                premierYear.FromISO8601String(item.json.PremiereDate)
+                item.subTitle = stri(premierYear.GetYear())
+            end if
+            item.Type = item.json.Type
+            row.appendChild(item)
+        end for
+        addRowSize([234, 396])
+    end if
+    ' Special Features next...
+    m.SpecialFeaturesTask.itemId = m.top.parentId
+    m.SpecialFeaturesTask.control = "RUN"
+end sub
+
+function onSpecialFeaturesLoaded()
+    data = m.SpecialFeaturesTask.content
+    m.SpecialFeaturesTask.unobserveField("content")
+    if data <> invalid and data.count() > 0
+        row = m.top.content.createChild("ContentNode")
+        row.Title = tr("Special Features")
+        for each item in data
+            m.top.visible = true
+            item.Id = item.json.Id
+            item.labelText = item.json.Name
+            item.subTitle = ""
+            item.Type = item.json.Type
+            item.imageWidth = 450
+            row.appendChild(item)
+        end for
+        addRowSize([462, 372])
+    end if
+
+    return m.top.content
+end function
+
+sub onMoviesLoaded()
+    data = m.LoadMoviesTask.content
+    m.LoadMoviesTask.unobserveField("content")
+    rlContent = CreateObject("roSGNode", "ContentNode")
+    if data <> invalid and data.count() > 0
+        row = rlContent.createChild("ContentNode")
+        row.title = tr("Movies")
+        for each mov in data
+            mov.Id = mov.json.Id
+            mov.labelText = mov.json.Name
+            mov.subTitle = mov.json.ProductionYear
+            mov.Type = mov.json.Type
+            row.appendChild(mov)
+        end for
+        m.top.rowItemSize = [[234, 396]]
+    end if
+    m.top.content = rlContent
+    m.LoadShowsTask.itemId = m.personId
+    m.LoadShowsTask.observeField("content", "onShowsLoaded")
+    m.LoadShowsTask.control = "RUN"
+end sub
+
+sub onShowsLoaded()
+    data = m.LoadShowsTask.content
+    m.LoadShowsTask.unobserveField("content")
+    if data <> invalid and data.count() > 0
+        row = buildRow("TV Shows", data, 502)
+        addRowSize([502, 396])
+        m.top.content.appendChild(row)
+    end if
+    m.LoadSeriesTask.itemId = m.personId
+    m.LoadSeriesTask.observeField("content", "onSeriesLoaded")
+    m.LoadSeriesTask.control = "RUN"
+end sub
+
+sub onSeriesLoaded()
+    data = m.LoadSeriesTask.content
+    m.LoadSeriesTask.unobserveField("content")
+    if data <> invalid and data.count() > 0
+        row = buildRow("Series", data)
+        addRowSize([234, 396])
+        m.top.content.appendChild(row)
+    end if
+    m.top.visible = true
+end sub
+
+function buildRow(rowTitle as string, items, imgWdth = 0)
+    row = CreateObject("roSGNode", "ContentNode")
+    row.Title = tr(rowTitle)
+    for each mov in items
+        mov.Id = mov.json.Id
+        mov.labelText = mov.json.Name
+        mov.subTitle = mov.json.ProductionYear
+        mov.Type = mov.json.Type
+        if imgWdth > 0
+            mov.imageWidth = imgWdth
+        end if
+        row.appendChild(mov)
+    end for
+    return row
+end function
+
+sub addRowSize(newRow)
+    sizeArray = m.top.rowItemSize
+    newSizeArray = []
+    for each size in sizeArray
+        newSizeArray.push(size)
+    end for
+    newSizeArray.push(newRow)
+    m.top.rowItemSize = newSizeArray
+end sub
+
+sub onRowItemSelected()
+    m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
+end sub
+
+sub onRowItemFocused()
+    m.top.focusedItem = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_home_Home.bs.html b/docs/api/components_home_Home.bs.html new file mode 100644 index 000000000..5685a0fd8 --- /dev/null +++ b/docs/api/components_home_Home.bs.html @@ -0,0 +1,64 @@ +Source: components/home/Home.bs
On this page

components_home_Home.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/deviceCapabilities.bs"
+
+sub init()
+    m.isFirstRun = true
+    m.top.overhangTitle = "Home"
+    m.top.optionsAvailable = true
+    m.postTask = createObject("roSGNode", "PostTask")
+
+    m.homeRows = m.top.findNode("homeRows")
+
+    m.fadeInFocusBitmap = m.top.findNode("fadeInFocusBitmap")
+
+    if m.global.session.user.settings["ui.home.splashBackground"] = true
+        m.backdrop = m.top.findNode("backdrop")
+        m.backdrop.uri = buildURL("/Branding/Splashscreen?format=jpg&foregroundLayer=0.15&fillWidth=1280&width=1280&fillHeight=720&height=720&tag=splash")
+    end if
+end sub
+
+sub refresh()
+    m.homeRows.focusBitmapBlendColor = "0xFFFFFFFF"
+    m.homeRows.callFunc("updateHomeRows")
+end sub
+
+sub loadLibraries()
+    m.homeRows.focusBitmapBlendColor = "0xFFFFFF00"
+    m.homeRows.callFunc("loadLibraries")
+    m.fadeInFocusBitmap.control = "start"
+end sub
+
+' JFScreen hook that gets ran as needed.
+' Used to update the focus, the state of the data, and tells the server about the device profile
+sub OnScreenShown()
+    if isValid(m.top.lastFocus)
+        m.top.lastFocus.setFocus(true)
+    else
+        m.top.setFocus(true)
+    end if
+
+    if not m.isFirstRun
+        refresh()
+    end if
+
+    ' post the device profile the first time this screen is loaded
+    if m.isFirstRun
+        m.isFirstRun = false
+        m.postTask.arrayData = getDeviceCapabilities()
+        m.postTask.apiUrl = "/Sessions/Capabilities/Full"
+        m.postTask.control = "RUN"
+        m.postTask.observeField("responseCode", "postFinished")
+    end if
+end sub
+
+' Triggered by m.postTask after completing a post.
+' Empty the task data when finished.
+sub postFinished()
+    m.postTask.unobserveField("responseCode")
+    m.postTask.callFunc("empty")
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_home_HomeItem.bs.html b/docs/api/components_home_HomeItem.bs.html new file mode 100644 index 000000000..a43e787c1 --- /dev/null +++ b/docs/api/components_home_HomeItem.bs.html @@ -0,0 +1,339 @@ +Source: components/home/HomeItem.bs
On this page

components_home_HomeItem.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+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("HomeItem")
+    m.itemText = m.top.findNode("itemText")
+    m.itemPoster = m.top.findNode("itemPoster")
+    m.itemProgress = m.top.findNode("progress")
+    m.itemProgressBackground = m.top.findNode("progressBackground")
+    m.itemIcon = m.top.findNode("itemIcon")
+    m.itemTextExtra = m.top.findNode("itemTextExtra")
+    m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
+    m.unplayedCount = m.top.findNode("unplayedCount")
+    m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
+    m.playedIndicator = m.top.findNode("playedIndicator")
+
+    m.showProgressBarAnimation = m.top.findNode("showProgressBar")
+    m.showProgressBarField = m.top.findNode("showProgressBarField")
+
+    ' Randomize the background colors
+    m.backdrop = m.top.findNode("backdrop")
+    posterBackgrounds = m.global.constants.poster_bg_pallet
+    m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
+end sub
+
+
+sub itemContentChanged()
+    m.unplayedCount.visible = false
+    itemData = m.top.itemContent
+    if itemData = invalid then return
+
+    itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem"
+
+    m.itemPoster.width = itemData.imageWidth
+    m.itemText.maxWidth = itemData.imageWidth
+    m.itemTextExtra.width = itemData.imageWidth
+    m.itemTextExtra.visible = true
+    m.itemTextExtra.text = ""
+
+    m.backdrop.width = itemData.imageWidth
+
+    if isValid(itemData.iconUrl)
+        m.itemIcon.uri = itemData.iconUrl
+    end if
+
+    if itemData.isWatched
+        m.playedIndicator.visible = true
+        m.unplayedCount.visible = false
+    else
+        m.playedIndicator.visible = false
+
+        if LCase(itemData.type) = "series"
+            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
+                        m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+                    end if
+                end if
+            end if
+        end if
+    end if
+
+    ' Format the Data based on the type of Home Data
+    if itemData.type = "CollectionFolder" or itemData.type = "UserView" or itemData.type = "Channel"
+        m.itemText.font.size = 35
+        m.itemText.height = 64
+        m.itemText.horizAlign = "center"
+        m.itemText.vertAlign = "bottom"
+        m.itemText.text = itemData.name
+        m.itemPoster.uri = itemData.widePosterURL
+        return
+    end if
+
+    if itemData.type = "UserView"
+        m.itemPoster.width = "96"
+        m.itemPoster.height = "96"
+        m.itemPoster.translation = "[192, 88]"
+        m.itemText.text = itemData.name
+        m.itemPoster.uri = itemData.widePosterURL
+        return
+    end if
+
+    playedIndicatorLeftPosition = m.itemPoster.width - 60
+    m.playedIndicator.translation = [playedIndicatorLeftPosition, 0]
+
+    m.itemText.height = 34
+    m.itemText.font.size = 25
+    m.itemText.horizAlign = "left"
+    m.itemText.vertAlign = "bottom"
+    m.itemTextExtra.visible = true
+    m.itemTextExtra.font.size = 22
+
+    ' "Program" is from clicking on an "On Now" item on the Home Screen
+    if itemData.type = "Program"
+        m.itemText.Text = itemData.json.name
+        m.itemTextExtra.Text = itemData.json.ChannelName
+        if itemData.usePoster
+            m.itemPoster.uri = itemData.thumbnailURL
+        else
+            m.itemPoster.uri = ImageURL(itemData.json.ChannelId)
+        end if
+        m.itemPoster.loadDisplayMode = "scaleToFill"
+
+        ' Set Episode title if available
+        if isValid(itemData.json.EpisodeTitle)
+            m.itemTextExtra.text = itemData.json.EpisodeTitle
+        end if
+
+        return
+    end if
+
+    if itemData.type = "Episode"
+        m.itemText.text = itemData.json.SeriesName
+
+        if itemData.PlayedPercentage > 0
+            drawProgressBar(itemData)
+        end if
+
+        if itemData.usePoster = true
+            m.itemPoster.uri = itemData.widePosterURL
+        else
+            m.itemPoster.uri = itemData.thumbnailURL
+        end if
+
+        ' Set Series and Episode Number for Extra Text
+        extraPrefix = ""
+        if isValid(itemData.json.ParentIndexNumber)
+            extraPrefix = "S" + StrI(itemData.json.ParentIndexNumber).trim()
+        end if
+        if isValid(itemData.json.IndexNumber)
+            extraPrefix = extraPrefix + "E" + StrI(itemData.json.IndexNumber).trim()
+        end if
+        if extraPrefix.len() > 0
+            extraPrefix = extraPrefix + " - "
+        end if
+
+        m.itemTextExtra.text = extraPrefix + itemData.name
+        return
+    end if
+
+    if itemData.type = "Movie" or itemData.type = "MusicVideo"
+        m.itemText.text = itemData.name
+
+        if itemData.PlayedPercentage > 0
+            drawProgressBar(itemData)
+        end if
+
+        ' Use best image, but fallback to secondary if it's empty
+        if (itemData.imageWidth = 180 and itemData.posterURL <> "") or itemData.thumbnailURL = ""
+            m.itemPoster.uri = itemData.posterURL
+        else
+            m.itemPoster.uri = itemData.thumbnailURL
+        end if
+
+        ' Set Release Year and Age Rating for Extra Text
+        textExtra = ""
+        if isValid(itemData.json.ProductionYear)
+            textExtra = StrI(itemData.json.ProductionYear).trim()
+        end if
+        if isValid(itemData.json.OfficialRating)
+            if textExtra <> ""
+                textExtra = textExtra + " - " + itemData.json.OfficialRating
+            else
+                textExtra = itemData.json.OfficialRating
+            end if
+        end if
+        m.itemTextExtra.text = textExtra
+
+        return
+    end if
+
+    if itemData.type = "Video"
+        m.itemText.text = itemData.name
+
+        if itemData.PlayedPercentage > 0
+            drawProgressBar(itemData)
+        end if
+
+        if itemData.imageWidth = 180
+            m.itemPoster.uri = itemData.posterURL
+        else
+            m.itemPoster.uri = itemData.thumbnailURL
+        end if
+
+        return
+    end if
+
+    if itemData.type = "BoxSet"
+        m.itemText.text = itemData.name
+        m.itemPoster.uri = itemData.posterURL
+
+        ' Set small text to number of items in the collection
+        if isValid(itemData.json) and isValid(itemData.json.ChildCount)
+            m.itemTextExtra.text = StrI(itemData.json.ChildCount).trim() + " item"
+            if itemData.json.ChildCount > 1
+                m.itemTextExtra.text += "s"
+            end if
+        end if
+        return
+    end if
+
+    if itemData.type = "Series"
+
+        m.itemText.text = itemData.name
+
+        if itemData.usePoster = true
+            if itemData.imageWidth = 180
+                m.itemPoster.uri = itemData.posterURL
+            else
+                m.itemPoster.uri = itemData.widePosterURL
+            end if
+        else
+            m.itemPoster.uri = itemData.thumbnailURL
+        end if
+
+        textExtra = ""
+        if isValid(itemData.json.ProductionYear)
+            textExtra = StrI(itemData.json.ProductionYear).trim()
+        end if
+
+        ' Set Years Run for Extra Text
+        if itemData.json.Status = "Continuing"
+            textExtra = textExtra + " - Present"
+        else if itemData.json.Status = "Ended" and isValid(itemData.json.EndDate)
+            textExtra = textExtra + " - " + LEFT(itemData.json.EndDate, 4)
+        end if
+        m.itemTextExtra.text = textExtra
+
+        return
+    end if
+
+    if itemData.type = "MusicAlbum"
+        m.itemText.text = itemData.name
+        m.itemTextExtra.text = itemData.json.AlbumArtist
+        m.itemPoster.uri = itemData.posterURL
+        return
+    end if
+
+    if itemData.type = "MusicArtist"
+        m.itemText.text = itemData.name
+        m.itemTextExtra.text = itemData.json.AlbumArtist
+        m.itemPoster.uri = ImageURL(itemData.id)
+        return
+    end if
+
+    if itemData.type = "Audio"
+        m.itemText.text = itemData.name
+        m.itemTextExtra.text = itemData.json.AlbumArtist
+        m.itemPoster.uri = ImageURL(itemData.id)
+        return
+    end if
+
+    if itemData.type = "TvChannel"
+        m.itemText.text = itemData.name
+        m.itemTextExtra.text = itemData.json.AlbumArtist
+        m.itemPoster.uri = ImageURL(itemData.id)
+        return
+    end if
+
+    if itemData.type = "Season"
+        m.itemText.text = itemData.json.SeriesName
+        m.itemTextExtra.text = itemData.name
+        m.itemPoster.uri = ImageURL(itemData.id)
+        return
+    end if
+
+    if itemData.type = "Photo"
+        m.itemText.text = itemData.name
+        m.itemPoster.uri = ImageURL(itemData.id)
+
+        ' subtext
+        if isValidAndNotEmpty(itemData.json)
+            if isValid(itemData.json.ProductionYear)
+                m.itemTextExtra.text = itemData.json.ProductionYear.ToStr().trim()
+            end if
+            if isValidAndNotEmpty(itemData.json.Album)
+                if m.itemTextExtra.text = ""
+                    m.itemTextExtra.text = tr("Album") + ": " + itemData.json.Album.trim()
+                else
+                    m.itemTextExtra.text = m.itemTextExtra.text + " - " + tr("Album") + ": " + itemData.json.Album.trim()
+                end if
+            end if
+        end if
+        return
+    end if
+
+    if itemData.type = "PhotoAlbum"
+        m.itemText.text = itemData.name
+        m.itemPoster.uri = ImageURL(itemData.id)
+
+        ' subtext
+        if isValid(itemData.json.ChildCount)
+            m.itemTextExtra.text = itemData.json.ChildCount.ToStr().trim() + " items"
+        end if
+
+        return
+    end if
+
+    m.log.warn("Unhandled Home Item Type", itemData.type)
+end sub
+
+'
+' Draws and animates item progress bar
+sub drawProgressBar(itemData)
+    m.itemProgressBackground.width = itemData.imageWidth
+    m.itemProgressBackground.visible = true
+    m.showProgressBarField.keyValue = [0, m.itemPoster.width * (itemData.PlayedPercentage / 100)]
+    m.showProgressBarAnimation.control = "Start"
+end sub
+
+'
+' Enable title scrolling based on item Focus
+sub focusChanged()
+
+    if m.top.itemHasFocus = true
+        m.itemText.repeatCount = -1
+    else
+        m.itemText.repeatCount = 0
+    end if
+
+end sub
+
+'Hide backdrop and icon when poster loaded
+sub onPosterLoadStatusChanged()
+    if m.itemPoster.loadStatus = "ready" and m.itemPoster.uri <> ""
+        m.backdrop.visible = false
+        m.itemIcon.visible = false
+    else
+        m.backdrop.visible = true
+        m.itemIcon.visible = true
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_home_HomeRows.bs.html b/docs/api/components_home_HomeRows.bs.html new file mode 100644 index 000000000..02dabab11 --- /dev/null +++ b/docs/api/components_home_HomeRows.bs.html @@ -0,0 +1,683 @@ +Source: components/home/HomeRows.bs
On this page

components_home_HomeRows.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/constants/HomeRowItemSizes.bs"
+
+const LOADING_WAIT_TIME = 2
+
+sub init()
+    m.top.itemComponentName = "HomeItem"
+    ' how many rows are visible on the screen
+    m.top.numRows = 2
+
+    m.top.rowFocusAnimationStyle = "fixedFocusWrap"
+    m.top.vertFocusAnimationStyle = "fixedFocus"
+
+    m.top.showRowLabel = [true]
+    m.top.rowLabelOffset = [0, 20]
+    ' Hide the row counter to prevent flicker. We'll show it once loading timer fires
+    m.top.showRowCounter = [false]
+
+    m.top.content = CreateObject("roSGNode", "ContentNode")
+
+    m.loadingTimer = createObject("roSGNode", "Timer")
+    m.loadingTimer.duration = LOADING_WAIT_TIME
+    m.loadingTimer.observeField("fire", "loadingTimerComplete")
+
+    updateSize()
+
+    m.top.setfocus(true)
+
+    m.top.observeField("rowItemSelected", "itemSelected")
+
+    ' Load the Libraries from API via task
+    m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask")
+    m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
+
+    ' set up task nodes for other rows
+    m.LoadContinueWatchingTask = createObject("roSGNode", "LoadItemsTask")
+    m.LoadContinueWatchingTask.itemsToLoad = "continue"
+
+    m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
+    m.LoadNextUpTask.itemsToLoad = "nextUp"
+
+    m.LoadOnNowTask = createObject("roSGNode", "LoadItemsTask")
+    m.LoadOnNowTask.itemsToLoad = "onNow"
+
+    m.LoadFavoritesTask = createObject("roSGNode", "LoadItemsTask")
+    m.LoadFavoritesTask.itemsToLoad = "favorites"
+end sub
+
+sub loadLibraries()
+    m.LoadLibrariesTask.control = "RUN"
+end sub
+
+sub updateSize()
+    m.top.translation = [111, 180]
+    itemHeight = 330
+
+    'Set width of Rows to cut off at edge of Safe Zone
+    m.top.itemSize = [1703, itemHeight]
+
+    ' spacing between rows
+    m.top.itemSpacing = [0, 105]
+
+    ' spacing between items in a row
+    m.top.rowItemSpacing = [20, 0]
+
+    ' Default size to wide poster, the most used size
+    m.top.rowItemSize = homeRowItemSizes.WIDE_POSTER
+
+    m.top.visible = true
+end sub
+
+' processUserSections: Loop through user's chosen home section settings and generate the content for each row
+'
+sub processUserSections()
+    m.expectedRowCount = 1 ' the favorites row is hardcoded to always show atm
+    m.processedRowCount = 0
+
+    ' calculate expected row count by processing homesections
+    for i = 0 to 6
+        sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
+        if sectionName = "latestmedia"
+            ' expect 1 row per filtered media library
+            m.filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
+            for each latestLibrary in m.filteredLatest
+                if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program"
+                    m.expectedRowCount++
+                end if
+            end for
+        else if sectionName <> "none"
+            m.expectedRowCount++
+        end if
+    end for
+
+    ' Add home sections in order based on user settings
+    loadedSections = 0
+    for i = 0 to 6
+        sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
+        sectionLoaded = false
+        if sectionName <> "none"
+            sectionLoaded = addHomeSection(sectionName)
+        end if
+
+        ' Count how many sections with data are loaded
+        if sectionLoaded then loadedSections++
+
+        ' If 2 sections with data are loaded or we're at the end of the web client section data, consider the home view loaded
+        if not m.global.app_loaded
+            if loadedSections = 2 or i = 6
+                m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
+                m.global.app_loaded = true
+            end if
+        end if
+    end for
+
+    ' Favorites isn't an option in Web settings, so we manually add it to the end for now
+    addHomeSection("favorites")
+
+    ' Start the timer for creating the content rows before we set the cursor size
+    m.loadingTimer.control = "start"
+end sub
+
+' onLibrariesLoaded: Handler when LoadLibrariesTask returns data
+'
+sub onLibrariesLoaded()
+    ' save data for other functions
+    m.libraryData = m.LoadLibrariesTask.content
+    m.LoadLibrariesTask.unobserveField("content")
+    m.LoadLibrariesTask.content = []
+
+    processUserSections()
+end sub
+
+' getOriginalSectionIndex: Gets the index of a section from user settings and adds count of currently known latest media sections
+'
+' @param {string} sectionName - Name of section we're looking up
+'
+' @return {integer} indicating index of section taking latest media sections into account
+function getOriginalSectionIndex(sectionName as string) as integer
+    searchSectionName = LCase(sectionName).Replace(" ", "")
+
+    sectionIndex = 0
+    indexLatestMediaSection = 0
+
+    for i = 0 to 6
+        settingSectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
+        if settingSectionName = "latestmedia"
+            indexLatestMediaSection = i
+        end if
+
+        if settingSectionName = searchSectionName
+            sectionIndex = i
+        end if
+    end for
+
+    ' If the latest media section is before the section we're searching for, then we need to account for how many latest media rows there are
+    addLatestMediaSectionCount = (indexLatestMediaSection < sectionIndex)
+
+    if addLatestMediaSectionCount
+        for i = sectionIndex to m.top.content.getChildCount() - 1
+            sectionToTest = m.top.content.getChild(i)
+            if LCase(Left(sectionToTest.title, 6)) = "latest"
+                sectionIndex++
+            end if
+        end for
+    end if
+
+    return sectionIndex
+end function
+
+' removeHomeSection: Removes a home section from the home rows
+'
+' @param {string} sectionToRemove - Title property of section we're removing
+sub removeHomeSection(sectionTitleToRemove as string)
+    if not isValid(sectionTitleToRemove) then return
+
+    sectionTitle = LCase(sectionTitleToRemove).Replace(" ", "")
+    if not sectionExists(sectionTitle) then return
+
+    sectionIndexToRemove = getSectionIndex(sectionTitle)
+
+    m.top.content.removeChildIndex(sectionIndexToRemove)
+    setRowItemSize()
+end sub
+
+' setRowItemSize: Loops through all home sections and sets the correct item sizes per row
+'
+sub setRowItemSize()
+    if not isValid(m.top.content) then return
+
+    homeSections = m.top.content.getChildren(-1, 0)
+    newSizeArray = CreateObject("roArray", homeSections.count(), false)
+
+    for i = 0 to homeSections.count() - 1
+        newSizeArray[i] = isValid(homeSections[i].cursorSize) ? homeSections[i].cursorSize : homeRowItemSizes.WIDE_POSTER
+    end for
+    m.top.rowItemSize = newSizeArray
+
+    ' If we have processed the expected number of content rows, stop the loading timer and run the complete function
+    if m.expectedRowCount = m.processedRowCount
+        m.loadingTimer.control = "stop"
+        loadingTimerComplete()
+    end if
+end sub
+
+' loadingTimerComplete: Event handler for when loading wait time has expired
+'
+sub loadingTimerComplete()
+    if not m.top.showRowCounter[0]
+        ' Show the row counter to prevent flicker
+        m.top.showRowCounter = [true]
+    end if
+end sub
+
+' addHomeSection: Adds a new home section to the home rows.
+'
+' @param {string} sectionType - Type of section to add
+' @return {boolean} indicating if the section was handled
+function addHomeSection(sectionType as string) as boolean
+    ' Poster size library items
+    if sectionType = "livetv"
+        createLiveTVRow()
+        return true
+    end if
+
+    ' Poster size library items
+    if sectionType = "smalllibrarytiles"
+        createLibraryRow()
+        return true
+    end if
+
+    ' Continue Watching items
+    if sectionType = "resume"
+        createContinueWatchingRow()
+        return true
+    end if
+
+    ' Next Up items
+    if sectionType = "nextup"
+        createNextUpRow()
+        return true
+    end if
+
+    ' Latest items in each library
+    if sectionType = "latestmedia"
+        createLatestInRows()
+        return true
+    end if
+
+    ' Favorite Items
+    if sectionType = "favorites"
+        createFavoritesRow()
+        return true
+    end if
+
+    ' This section type isn't supported.
+    ' Count it as processed since we aren't going to do anything else with it
+    m.processedRowCount++
+    return false
+end function
+
+' createLibraryRow: Creates a row displaying the user's libraries
+'
+sub createLibraryRow()
+    m.processedRowCount++
+    ' Ensure we have data
+    if not isValidAndNotEmpty(m.libraryData) then return
+
+    sectionName = tr("My Media")
+
+    ' We don't refresh library data, so if section already exists, exit
+    if sectionExists(sectionName)
+        return
+    end if
+
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = sectionName
+    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+    row.cursorSize = homeRowItemSizes.WIDE_POSTER
+
+    filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
+    for each item in filteredMedia
+        row.appendChild(item)
+    end for
+
+    ' Row does not exist, insert it into the home view
+    m.top.content.insertChild(row, getOriginalSectionIndex("smalllibrarytiles"))
+    setRowItemSize()
+end sub
+
+' createLatestInRows: Creates a row displaying latest items in each of the user's libraries
+'
+sub createLatestInRows()
+    ' Ensure we have data
+    if not isValidAndNotEmpty(m.libraryData) then return
+
+    ' create a "Latest In" row for each library
+    for each lib in m.filteredLatest
+        if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
+            loadLatest = createObject("roSGNode", "LoadItemsTask")
+            loadLatest.itemsToLoad = "latest"
+            loadLatest.itemId = lib.id
+
+            metadata = { "title": lib.name }
+            metadata.Append({ "contentType": lib.json.CollectionType })
+            loadLatest.metadata = metadata
+
+            loadLatest.observeField("content", "updateLatestItems")
+            loadLatest.control = "RUN"
+        end if
+    end for
+end sub
+
+' sectionExists: Checks if passed section exists in home row content
+'
+' @param {string} sectionTitle - Title of section we're checking for
+'
+' @return {boolean} indicating if the section currently exists in the home row content
+function sectionExists(sectionTitle as string) as boolean
+    if not isValid(sectionTitle) then return false
+    if not isValid(m.top.content) then return false
+
+    searchSectionTitle = LCase(sectionTitle).Replace(" ", "")
+
+    homeSections = m.top.content.getChildren(-1, 0)
+
+    for each section in homeSections
+        if LCase(section.title).Replace(" ", "") = searchSectionTitle
+            return true
+        end if
+    end for
+
+    return false
+end function
+
+' getSectionIndex: Returns index of requested section in home row content
+'
+' @param {string} sectionTitle - Title of section we're checking for
+'
+' @return {integer} indicating index of request section
+function getSectionIndex(sectionTitle as string) as integer
+    if not isValid(sectionTitle) then return false
+    if not isValid(m.top.content) then return false
+
+    searchSectionTitle = LCase(sectionTitle).Replace(" ", "")
+
+    homeSections = m.top.content.getChildren(-1, 0)
+
+    sectionIndex = homeSections.count()
+    i = 0
+
+    for each section in homeSections
+        if LCase(section.title).Replace(" ", "") = searchSectionTitle
+            sectionIndex = i
+            exit for
+        end if
+        i++
+    end for
+
+    return sectionIndex
+end function
+
+' createLiveTVRow: Creates a row displaying the live tv now on section
+'
+sub createLiveTVRow()
+    m.LoadOnNowTask.observeField("content", "updateOnNowItems")
+    m.LoadOnNowTask.control = "RUN"
+end sub
+
+' createContinueWatchingRow: Creates a row displaying items the user can continue watching
+'
+sub createContinueWatchingRow()
+    ' Load the Continue Watching Data
+    m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
+    m.LoadContinueWatchingTask.control = "RUN"
+end sub
+
+' createNextUpRow: Creates a row displaying next episodes up to watch
+'
+sub createNextUpRow()
+    sectionName = tr("Next Up") + ">"
+
+    if not sectionExists(sectionName)
+        nextUpRow = m.top.content.CreateChild("HomeRow")
+        nextUpRow.title = sectionName
+        nextUpRow.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+        nextUpRow.cursorSize = homeRowItemSizes.WIDE_POSTER
+    end if
+
+    ' Load the Next Up Data
+    m.LoadNextUpTask.observeField("content", "updateNextUpItems")
+    m.LoadNextUpTask.control = "RUN"
+end sub
+
+' createFavoritesRow: Creates a row displaying items from the user's favorites list
+'
+sub createFavoritesRow()
+    ' Load the Favorites Data
+    m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
+    m.LoadFavoritesTask.control = "RUN"
+end sub
+
+' updateHomeRows: Update function exposed to outside components
+'
+sub updateHomeRows()
+    ' Hide the row counter to prevent flicker. We'll show it once loading timer fires
+    m.top.showRowCounter = [false]
+    processUserSections()
+end sub
+
+' updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed
+'
+sub updateFavoritesItems()
+    m.processedRowCount++
+    itemData = m.LoadFavoritesTask.content
+    m.LoadFavoritesTask.unobserveField("content")
+    m.LoadFavoritesTask.content = []
+
+    sectionName = tr("Favorites")
+
+    if not isValidAndNotEmpty(itemData)
+        removeHomeSection(sectionName)
+        return
+    end if
+
+    ' remake row using the new data
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = sectionName
+    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+    row.cursorSize = homeRowItemSizes.WIDE_POSTER
+
+    for each item in itemData
+        usePoster = true
+
+        if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
+            usePoster = false
+        end if
+
+        item.usePoster = usePoster
+        item.imageWidth = row.imageWidth
+        row.appendChild(item)
+    end for
+
+    if sectionExists(sectionName)
+        m.top.content.replaceChild(row, getSectionIndex(sectionName))
+        setRowItemSize()
+        return
+    end if
+
+    m.top.content.insertChild(row, getSectionIndex(sectionName))
+    setRowItemSize()
+end sub
+
+' updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed
+'
+sub updateContinueWatchingItems()
+    m.processedRowCount++
+    itemData = m.LoadContinueWatchingTask.content
+    m.LoadContinueWatchingTask.unobserveField("content")
+    m.LoadContinueWatchingTask.content = []
+
+    sectionName = tr("Continue Watching")
+
+    if not isValidAndNotEmpty(itemData)
+        removeHomeSection(sectionName)
+        return
+    end if
+
+    sectionName = tr("Continue Watching")
+
+    ' remake row using the new data
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = sectionName
+    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+    row.cursorSize = homeRowItemSizes.WIDE_POSTER
+
+    for each item in itemData
+        if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
+            item.PlayedPercentage = item.json.UserData.PlayedPercentage
+        end if
+
+        item.usePoster = row.usePoster
+        item.imageWidth = row.imageWidth
+        row.appendChild(item)
+    end for
+
+    ' Row already exists, replace it with new content
+    if sectionExists(sectionName)
+        m.top.content.replaceChild(row, getSectionIndex(sectionName))
+        setRowItemSize()
+        return
+    end if
+
+    ' Row does not exist, insert it into the home view
+    m.top.content.insertChild(row, getOriginalSectionIndex("resume"))
+    setRowItemSize()
+end sub
+
+' updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed
+'
+sub updateNextUpItems()
+    m.processedRowCount++
+    itemData = m.LoadNextUpTask.content
+    m.LoadNextUpTask.unobserveField("content")
+    m.LoadNextUpTask.content = []
+    m.LoadNextUpTask.control = "STOP"
+
+    sectionName = tr("Next Up") + " >"
+
+    if not isValidAndNotEmpty(itemData)
+        removeHomeSection(sectionName)
+        return
+    end if
+
+    ' remake row using the new data
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = tr("Next Up") + " >"
+    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+    row.cursorSize = homeRowItemSizes.WIDE_POSTER
+
+    for each item in itemData
+        item.usePoster = row.usePoster
+        item.imageWidth = row.imageWidth
+        row.appendChild(item)
+    end for
+
+    ' Row already exists, replace it with new content
+    if sectionExists(sectionName)
+        m.top.content.replaceChild(row, getSectionIndex(sectionName))
+        setRowItemSize()
+        return
+    end if
+
+    ' Row does not exist, insert it into the home view
+    m.top.content.insertChild(row, getSectionIndex(sectionName))
+    setRowItemSize()
+end sub
+
+' updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed
+'
+' @param {dynamic} msg - LoadItemsTask
+sub updateLatestItems(msg)
+    m.processedRowCount++
+    itemData = msg.GetData()
+
+    node = msg.getRoSGNode()
+    node.unobserveField("content")
+    node.content = []
+
+    sectionName = tr("Latest in") + " " + node.metadata.title + " >"
+
+    if not isValidAndNotEmpty(itemData)
+        removeHomeSection(sectionName)
+        return
+    end if
+
+    imagesize = homeRowItemSizes.WIDE_POSTER
+
+    if isValid(node.metadata.contentType)
+        if LCase(node.metadata.contentType) = "movies"
+            imagesize = homeRowItemSizes.MOVIE_POSTER
+        else if LCase(node.metadata.contentType) = "music"
+            imagesize = homeRowItemSizes.MUSIC_ALBUM
+        end if
+    end if
+
+    ' remake row using new data
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = sectionName
+    row.imageWidth = imagesize[0]
+    row.cursorSize = imagesize
+    row.usePoster = true
+
+    for each item in itemData
+        item.usePoster = row.usePoster
+        item.imageWidth = row.imageWidth
+        row.appendChild(item)
+    end for
+
+    if sectionExists(sectionName)
+        ' Row already exists, replace it with new content
+        m.top.content.replaceChild(row, getSectionIndex(sectionName))
+        setRowItemSize()
+        return
+    end if
+
+    m.top.content.insertChild(row, getOriginalSectionIndex("latestmedia"))
+    setRowItemSize()
+end sub
+
+' updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed
+'
+sub updateOnNowItems()
+    m.processedRowCount++
+    itemData = m.LoadOnNowTask.content
+    m.LoadOnNowTask.unobserveField("content")
+    m.LoadOnNowTask.content = []
+
+    sectionName = tr("On Now")
+
+    if not isValidAndNotEmpty(itemData)
+        removeHomeSection(sectionName)
+        return
+    end if
+
+    ' remake row using the new data
+    row = CreateObject("roSGNode", "HomeRow")
+    row.title = tr("On Now")
+    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
+    row.cursorSize = homeRowItemSizes.WIDE_POSTER
+
+    for each item in itemData
+        row.usePoster = false
+
+        if (not isValid(item.thumbnailURL) or item.thumbnailURL = "") and isValid(item.json) and isValid(item.json.imageURL)
+            item.thumbnailURL = item.json.imageURL
+            row.usePoster = true
+            row.imageWidth = homeRowItemSizes.MOVIE_POSTER[0]
+            row.cursorSize = homeRowItemSizes.MOVIE_POSTER
+        end if
+
+        item.usePoster = row.usePoster
+        item.imageWidth = row.imageWidth
+        row.appendChild(item)
+    end for
+
+    ' Row already exists, replace it with new content
+    if sectionExists(sectionName)
+        m.top.content.replaceChild(row, getSectionIndex(sectionName))
+        setRowItemSize()
+        return
+    end if
+
+    ' Row does not exist, insert it into the home view
+    m.top.content.insertChild(row, getOriginalSectionIndex("livetv"))
+    setRowItemSize()
+end sub
+
+sub itemSelected()
+    m.selectedRowItem = m.top.rowItemSelected
+
+    m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
+
+    'Prevent the selected item event from double firing
+    m.top.selectedItem = invalid
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if press
+        if key = "play"
+            print "play was pressed from homerow"
+            itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
+            if isValid(itemToPlay)
+                m.top.quickPlayNode = itemToPlay
+            end if
+            return true
+        else if key = "replay"
+            m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0]
+            return true
+        end if
+    end if
+    return false
+end function
+
+function filterNodeArray(nodeArray as object, nodeKey as string, excludeArray as object) as object
+    if excludeArray.IsEmpty() then return nodeArray
+
+    newNodeArray = []
+    for each node in nodeArray
+        excludeThisNode = false
+        for each exclude in excludeArray
+            if node[nodeKey] = exclude
+                excludeThisNode = true
+            end if
+        end for
+        if excludeThisNode = false
+            newNodeArray.Push(node)
+        end if
+    end for
+    return newNodeArray
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_home_LoadItemsTask.bs.html b/docs/api/components_home_LoadItemsTask.bs.html new file mode 100644 index 000000000..31c62a390 --- /dev/null +++ b/docs/api/components_home_LoadItemsTask.bs.html @@ -0,0 +1,271 @@ +Source: components/home/LoadItemsTask.bs
On this page

components_home_LoadItemsTask.bs

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/utils/deviceCapabilities.bs"
+import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/sdk.bs"
+
+sub init()
+    m.top.functionName = "loadItems"
+end sub
+
+sub loadItems()
+
+    results = []
+
+    ' Load Libraries
+    if m.top.itemsToLoad = "libraries"
+
+        url = Substitute("Users/{0}/Views/", m.global.session.user.id)
+        resp = APIRequest(url)
+        data = getJson(resp)
+        if isValid(data) and isValid(data.Items)
+            for each item in data.Items
+                ' Skip Books for now as we don't support it (issue #525)
+                if item.CollectionType <> "books"
+                    tmp = CreateObject("roSGNode", "HomeData")
+                    tmp.json = item
+                    results.push(tmp)
+                end if
+            end for
+        end if
+
+        ' Load Latest Additions to Libraries
+    else if m.top.itemsToLoad = "latest"
+        activeUser = m.global.session.user.id
+        if isValid(activeUser)
+            url = Substitute("Users/{0}/Items/Latest", activeUser)
+            params = {}
+            params["Limit"] = 16
+            params["ParentId"] = m.top.itemId
+            params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
+            params["ImageTypeLimit"] = 1
+            params["EnableTotalRecordCount"] = false
+
+            resp = APIRequest(url, params)
+            data = getJson(resp)
+
+            if isValid(data)
+                for each item in data
+                    ' Skip Books for now as we don't support it (issue #525)
+                    if item.Type <> "Book"
+                        tmp = CreateObject("roSGNode", "HomeData")
+                        tmp.json = item
+                        results.push(tmp)
+                    end if
+                end for
+            end if
+        end if
+
+        ' Load Next Up
+    else if m.top.itemsToLoad = "nextUp"
+
+        url = "Shows/NextUp"
+        params = {}
+        params["recursive"] = true
+        params["SortBy"] = "DatePlayed"
+        params["SortOrder"] = "Descending"
+        params["ImageTypeLimit"] = 1
+        params["UserId"] = m.global.session.user.id
+        params["EnableRewatching"] = false
+        params["DisableFirstEpisode"] = false
+        params["limit"] = 24
+        params["EnableTotalRecordCount"] = false
+
+        maxDaysInNextUp = m.global.session.user.settings["ui.details.maxdaysnextup"].ToInt()
+        if isValid(maxDaysInNextUp)
+            if maxDaysInNextUp > 0
+                dateToday = CreateObject("roDateTime")
+                dateCutoff = CreateObject("roDateTime")
+
+                dateCutoff.FromSeconds(dateToday.AsSeconds() - (maxDaysInNextUp * 86400))
+
+                params["NextUpDateCutoff"] = dateCutoff.ToISOString()
+            end if
+        end if
+
+        resp = APIRequest(url, params)
+        data = getJson(resp)
+        if isValid(data) and isValid(data.Items)
+            for each item in data.Items
+                tmp = CreateObject("roSGNode", "HomeData")
+                tmp.json = item
+                results.push(tmp)
+            end for
+        end if
+        ' Load Continue Watching
+    else if m.top.itemsToLoad = "continue"
+        activeUser = m.global.session.user.id
+        if isValid(activeUser)
+            url = Substitute("Users/{0}/Items/Resume", activeUser)
+
+            params = {}
+            params["recursive"] = true
+            params["SortBy"] = "DatePlayed"
+            params["SortOrder"] = "Descending"
+            params["Filters"] = "IsResumable"
+            params["EnableTotalRecordCount"] = false
+
+            resp = APIRequest(url, params)
+            data = getJson(resp)
+            if isValid(data) and isValid(data.Items)
+                for each item in data.Items
+                    ' Skip Books for now as we don't support it (issue #558)
+                    if item.Type <> "Book"
+                        tmp = CreateObject("roSGNode", "HomeData")
+                        tmp.json = item
+                        results.push(tmp)
+                    end if
+                end for
+            end if
+        end if
+
+    else if m.top.itemsToLoad = "favorites"
+
+        url = Substitute("Users/{0}/Items", m.global.session.user.id)
+
+        params = {}
+        params["Filters"] = "IsFavorite"
+        params["Limit"] = 20
+        params["recursive"] = true
+        params["sortby"] = "random"
+        params["EnableTotalRecordCount"] = false
+
+        resp = APIRequest(url, params)
+        data = getJson(resp)
+        if isValid(data) and isValid(data.Items)
+            for each item in data.Items
+                ' Skip Books for now as we don't support it (issue #558)
+                ' also skip songs since there is limited space
+                if not (item.Type = "Book" or item.Type = "Audio")
+                    tmp = CreateObject("roSGNode", "HomeData")
+
+                    params = {}
+                    params["Tags"] = item.PrimaryImageTag
+                    params["MaxWidth"] = 234
+                    params["MaxHeight"] = 330
+                    tmp.posterURL = ImageUrl(item.Id, "Primary", params)
+                    tmp.json = item
+                    results.push(tmp)
+                end if
+            end for
+        end if
+
+    else if m.top.itemsToLoad = "onNow"
+        url = "LiveTv/Programs/Recommended"
+        params = {}
+        params["userId"] = m.global.session.user.id
+        params["isAiring"] = true
+        params["limit"] = 16 ' 16 to be consistent with "Latest In"
+        params["imageTypeLimit"] = 1
+        params["enableImageTypes"] = "Primary,Thumb,Backdrop"
+        params["enableTotalRecordCount"] = false
+        params["fields"] = "ChannelInfo,PrimaryImageAspectRatio"
+
+        resp = APIRequest(url, params)
+        data = getJson(resp)
+        if isValid(data) and isValid(data.Items)
+            for each item in data.Items
+                tmp = CreateObject("roSGNode", "HomeData")
+                item.ImageURL = ImageURL(item.Id)
+                tmp.json = item
+                results.push(tmp)
+            end for
+        end if
+
+        ' Extract array of persons from Views and download full metadata for each
+    else if m.top.itemsToLoad = "people"
+        for each person in m.top.peopleList
+            tmp = CreateObject("roSGNode", "ExtrasData")
+            tmp.Id = person.Id
+            tmp.labelText = person.Name
+            params = {}
+            params["Tags"] = person.PrimaryImageTag
+            params["MaxWidth"] = 234
+            params["MaxHeight"] = 330
+            tmp.posterURL = ImageUrl(person.Id, "Primary", params)
+            tmp.json = person
+            results.push(tmp)
+        end for
+    else if m.top.itemsToLoad = "specialfeatures"
+        params = {}
+        url = Substitute("Users/{0}/Items/{1}/SpecialFeatures", m.global.session.user.id, m.top.itemId)
+        resp = APIRequest(url, params)
+        data = getJson(resp)
+        if data <> invalid and data.count() > 0
+            for each specfeat in data
+                tmp = CreateObject("roSGNode", "ExtrasData")
+                results.push(tmp)
+                params = {}
+                params["Tags"] = specfeat.ImageTags.Primary
+                params["MaxWidth"] = 450
+                params["MaxHeight"] = 402
+                tmp.posterURL = ImageUrl(specfeat.Id, "Primary", params)
+                tmp.json = specfeat
+            end for
+        end if
+    else if m.top.itemsToLoad = "additionalparts"
+        additionalParts = api.videos.GetAdditionalParts(m.top.itemId)
+        if isValid(additionalParts)
+            for each part in additionalParts.items
+                tmp = CreateObject("roSGNode", "ExtrasData")
+                params = {}
+                params["Tags"] = part.ImageTags.Primary
+                params["MaxWidth"] = 450
+                params["MaxHeight"] = 402
+                tmp.posterURL = ImageUrl(part.Id, "Primary", params)
+                tmp.json = part
+                results.push(tmp)
+            end for
+        end if
+    else if m.top.itemsToLoad = "likethis"
+        params = { "userId": m.global.session.user.id, "limit": 16 }
+        url = Substitute("Items/{0}/Similar", m.top.itemId)
+        resp = APIRequest(url, params)
+        data = getJson(resp)
+        if isValid(data) and isValid(data.Items)
+            for each item in data.items
+                tmp = CreateObject("roSGNode", "ExtrasData")
+                tmp.posterURL = ImageUrl(item.Id, "Primary", { "Tags": item.PrimaryImageTag })
+                tmp.json = item
+                results.push(tmp)
+            end for
+        end if
+    else if m.top.itemsToLoad = "personMovies"
+        getPersonVideos("Movie", results, {})
+    else if m.top.itemsToLoad = "personTVShows"
+        getPersonVideos("Episode", results, { MaxWidth: 502, MaxHeight: 300 })
+    else if m.top.itemsToLoad = "personSeries"
+        getPersonVideos("Series", results, {})
+    else if m.top.itemsToLoad = "metaData"
+        results.push(ItemMetaData(m.top.itemId))
+    else if m.top.itemsToLoad = "audioStream"
+        results.push(AudioStream(m.top.itemId))
+    else if m.top.itemsToLoad = "backdropImage"
+        results.push(BackdropImage(m.top.itemId))
+    end if
+
+    m.top.content = results
+
+end sub
+
+sub getPersonVideos(videoType, dest, dimens)
+    params = { personIds: m.top.itemId, recursive: true, includeItemTypes: videoType, Limit: 50, SortBy: "Random" }
+    url = Substitute("Users/{0}/Items", m.global.session.user.id)
+    resp = APIRequest(url, params)
+    data = getJson(resp)
+    if data <> invalid and data.count() > 0
+        for each item in data.items
+            tmp = CreateObject("roSGNode", "ExtrasData")
+            imgParms = { "Tags": item.ImageTags.Primary }
+            imgParms.append(dimens)
+            tmp.posterURL = ImageUrl(item.Id, "Primary", imgParms)
+            tmp.json = item
+            dest.push(tmp)
+        end for
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_keyboards_IntegerKeyboard.bs.html b/docs/api/components_keyboards_IntegerKeyboard.bs.html new file mode 100644 index 000000000..040654278 --- /dev/null +++ b/docs/api/components_keyboards_IntegerKeyboard.bs.html @@ -0,0 +1,84 @@ +Source: components/keyboards/IntegerKeyboard.bs
On this page

components_keyboards_IntegerKeyboard.bs

sub init()
+    m.top.keyGrid.keyDefinitionUri = "pkg:/components/keyboards/IntegerKeyboardKDF.json"
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if key = "back"
+        m.top.escape = key
+        return true
+    end if
+
+    if not press then return false
+
+    if key = "left"
+        if m.top.textEditBox.hasFocus()
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "1"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "4"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "7"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "backspace"
+            m.top.escape = key
+            return true
+        end if
+    end if
+
+    if key = "right"
+        if m.top.textEditBox.hasFocus()
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "3"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "6"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "9"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "submit"
+            m.top.escape = key
+            return true
+        end if
+    end if
+
+    if key = "up"
+        if m.top.textEditBox.hasFocus()
+            m.top.escape = key
+            return true
+        end if
+    end if
+
+    if key = "down"
+        if m.top.focusedChild.keyFocused = "0"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "backspace"
+            m.top.escape = key
+            return true
+        else if m.top.focusedChild.keyFocused = "submit"
+            m.top.escape = key
+            return true
+        end if
+    end if
+
+    return false
+end function
+
+function keySelected(key as string) as boolean
+    if key = "submit"
+        m.top.submit = true
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_LoadChannelsTask.bs.html b/docs/api/components_liveTv_LoadChannelsTask.bs.html new file mode 100644 index 000000000..23f1421fa --- /dev/null +++ b/docs/api/components_liveTv_LoadChannelsTask.bs.html @@ -0,0 +1,75 @@ +Source: components/liveTv/LoadChannelsTask.bs
On this page

components_liveTv_LoadChannelsTask.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.functionName = "loadChannels"
+end sub
+
+sub loadChannels()
+
+    results = []
+
+    sort_field = m.top.sortField
+
+    if m.top.sortAscending = true
+        sort_order = "Ascending"
+    else
+        sort_order = "Descending"
+    end if
+
+    params = {
+        includeItemTypes: "LiveTvChannel",
+        SortBy: sort_field,
+        SortOrder: sort_order,
+        recursive: m.top.recursive,
+        UserId: m.global.session.user.id
+    }
+
+    ' Handle special case when getting names starting with numeral
+    if m.top.NameStartsWith <> ""
+        if m.top.NameStartsWith = "#"
+            params.searchterm = "A"
+        else
+            params.searchterm = m.top.nameStartsWith
+        end if
+    end if
+
+    'Append voice search when there is text
+    if m.top.searchTerm <> ""
+        params.searchTerm = m.top.searchTerm
+    end if
+
+    if m.top.filter = "Favorites"
+        params.append({ isFavorite: true })
+    end if
+
+    url = Substitute("Users/{0}/Items/", m.global.session.user.id)
+
+    resp = APIRequest(url, params)
+    data = getJson(resp)
+
+    if data.TotalRecordCount = invalid
+        m.top.channels = results
+        return
+    end if
+
+
+    for each item in data.Items
+        channel = createObject("roSGNode", "ChannelData")
+        channel.json = item
+        if item.UserData <> invalid and item.UserData.isFavorite <> invalid
+            channel.favorite = item.UserData.isFavorite
+            if channel.favorite = true
+                results.Unshift(channel)
+            else
+                results.push(channel)
+            end if
+        else
+            results.push(channel)
+        end if
+    end for
+    m.top.channels = results
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_LoadProgramDetailsTask.bs.html b/docs/api/components_liveTv_LoadProgramDetailsTask.bs.html new file mode 100644 index 000000000..7d1518f65 --- /dev/null +++ b/docs/api/components_liveTv_LoadProgramDetailsTask.bs.html @@ -0,0 +1,47 @@ +Source: components/liveTv/LoadProgramDetailsTask.bs
On this page

components_liveTv_LoadProgramDetailsTask.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.functionName = "loadProgramDetails"
+
+end sub
+
+sub loadProgramDetails()
+
+    channelIndex = m.top.ChannelIndex
+    programIndex = m.top.ProgramIndex
+
+    params = {
+        UserId: m.global.session.user.id
+    }
+
+    url = Substitute("LiveTv/Programs/{0}", m.top.programId)
+
+    resp = APIRequest(url, params)
+    data = getJson(resp)
+
+    if data = invalid
+        m.top.programDetails = {}
+        return
+    end if
+
+    program = createObject("roSGNode", "ScheduleProgramData")
+    program.json = data
+    program.channelIndex = channelIndex
+    program.programIndex = programIndex
+    program.fullyLoaded = true
+    ' Are we currently recording this program?
+    if program.json.TimerId <> invalid and program.json.TimerId <> ""
+        ' This is needed here because the callee (onProgramDetailsLoaded) replaces the grid item with
+        ' this newly created item from the server, without this, the red icon
+        ' disappears when the user focuses on the program in question
+        program.hdSmallIconUrl = "pkg:/images/red.png"
+    else
+        program.hdSmallIconUrl = invalid
+    end if
+    m.top.programDetails = program
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_LoadSheduleTask.bs.html b/docs/api/components_liveTv_LoadSheduleTask.bs.html new file mode 100644 index 000000000..ccea01dc1 --- /dev/null +++ b/docs/api/components_liveTv_LoadSheduleTask.bs.html @@ -0,0 +1,53 @@ +Source: components/liveTv/LoadSheduleTask.bs
On this page

components_liveTv_LoadSheduleTask.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.functionName = "loadSchedule"
+end sub
+
+sub loadSchedule()
+
+    results = []
+
+    params = {
+        UserId: m.global.session.user.id,
+        SortBy: "startDate",
+        EnableImages: false,
+        EnableTotalRecordCount: false,
+        EnableUserData: false,
+        channelIds: m.top.channelIds,
+        MaxStartDate: m.top.endTime,
+        MinEndDate: m.top.startTime
+    }
+
+    url = "LiveTv/Programs"
+
+    resp = APIRequest(url)
+    data = postJson(resp, FormatJson(params))
+
+    if data = invalid
+        m.top.schedule = results
+        return
+    end if
+
+    results = []
+
+    for each item in data.Items
+        program = createObject("roSGNode", "ScheduleProgramData")
+        program.json = item
+        ' Are we currently recording this program?
+        if program.json.TimerId <> invalid and program.json.TimerId <> ""
+            program.hdSmallIconUrl = "pkg:/images/red.png"
+        else
+            program.hdSmallIconUrl = invalid
+        end if
+        results.push(program)
+    end for
+
+
+    m.top.schedule = results
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_ProgramDetails.bs.html b/docs/api/components_liveTv_ProgramDetails.bs.html new file mode 100644 index 000000000..8bbb925be --- /dev/null +++ b/docs/api/components_liveTv_ProgramDetails.bs.html @@ -0,0 +1,377 @@ +Source: components/liveTv/ProgramDetails.bs
On this page

components_liveTv_ProgramDetails.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+
+    ' Max "Overview" lines to show in Preview and Detail
+    m.maxPreviewLines = 5
+    m.maxDetailLines = 14
+
+    m.detailsView = m.top.findNode("detailsView")
+    m.noInfoView = m.top.findNode("noInformation")
+
+    m.programName = m.top.findNode("programName")
+    m.episodeTitle = m.top.findNode("episodeTitle")
+    m.episodeNumber = m.top.findNode("episodeNumber")
+    m.overview = m.top.findNode("overview")
+
+    m.episodeDetailsGroup = m.top.findNode("episodeDetailsGroup")
+    m.isLiveGroup = m.top.findNode("isLive")
+    m.isRepeatGroup = m.top.findNode("isRepeat")
+
+    m.broadcastDetails = m.top.findNode("broadcastDetails")
+    m.duration = m.top.findNode("duration")
+    m.channelName = m.top.findNode("channelName")
+    m.image = m.top.findNode("image")
+    m.favorite = m.top.findNode("favorite")
+
+    m.viewChannelFocusAnimationOpacity = m.top.findNode("viewChannelFocusAnimationOpacity")
+    m.recordFocusAnimationOpacity = m.top.findNode("recordFocusAnimationOpacity")
+    m.recordSeriesFocusAnimationOpacity = m.top.findNode("recordSeriesFocusAnimationOpacity")
+    m.focusAnimation = m.top.findNode("focusAnimation")
+
+    m.viewChannelButton = m.top.findNode("viewChannelButton")
+    m.recordButton = m.top.findNode("recordButton")
+    m.recordSeriesButton = m.top.findNode("recordSeriesButton")
+
+    m.viewChannelOutline = m.top.findNode("viewChannelOutline")
+    m.recordOutline = m.top.findNode("recordOutline")
+    m.recordSeriesOutline = m.top.findNode("recordSeriesOutline")
+
+    m.viewChannelLabel = m.top.findNode("viewChannelButtonLabel")
+    m.recordLabel = m.top.findNode("recordButtonLabel")
+    m.recordSeriesLabel = m.top.findNode("recordSeriesButtonLabel")
+
+    m.viewChannelButtonBackground = m.top.findNode("viewChannelButtonBackground")
+    m.recordButtonBackground = m.top.findNode("recordButtonBackground")
+    m.recordSeriesButtonBackground = m.top.findNode("recordSeriesButtonBackground")
+
+    m.focusAnimation.observeField("state", "onAnimationComplete")
+
+    setupLabels()
+end sub
+
+
+' Set up Live and Repeat label sizes
+sub setupLabels()
+
+    boundingRect = m.top.findNode("isLiveText").boundingRect()
+    isLiveBackground = m.top.findNode("isLiveBackground")
+    isLiveBackground.width = boundingRect.width + 16
+    isLiveBackground.height = boundingRect.height + 8
+    m.episodeDetailsGroup.removeChildIndex(0)
+
+    boundingRect = m.top.findNode("isRepeatText").boundingRect()
+    isRepeatBackground = m.top.findNode("isRepeatBackground")
+    isRepeatBackground.width = boundingRect.width + 16
+    isRepeatBackground.height = boundingRect.height + 8
+    m.episodeDetailsGroup.removeChildIndex(0)
+
+    m.viewChannelLabel.text = tr("View Channel")
+    boundingRect = m.viewChannelButton.boundingRect()
+    viewButtonBackground = m.top.findNode("viewChannelButtonBackground")
+    viewButtonBackground.width = boundingRect.width + 20
+    viewButtonBackground.height = boundingRect.height + 20
+    m.viewChannelOutline.width = viewButtonBackground.width
+    m.viewChannelOutline.height = viewButtonBackground.height
+
+    m.recordLabel.text = tr("Record")
+    boundingRect = m.recordButton.boundingRect()
+    recordButtonBackground = m.top.findNode("recordButtonBackground")
+    recordButtonBackground.width = boundingRect.width + 20
+    recordButtonBackground.height = boundingRect.height + 20
+    m.recordOutline.width = recordButtonBackground.width
+    m.recordOutline.height = recordButtonBackground.height
+
+    m.recordSeriesLabel.text = tr("Record Series")
+    boundingRect = m.recordSeriesButton.boundingRect()
+    recordSeriesButtonBackground = m.top.findNode("recordSeriesButtonBackground")
+    recordSeriesButtonBackground.width = boundingRect.width + 20
+    recordSeriesButtonBackground.height = boundingRect.height + 20
+    m.recordSeriesOutline.width = recordSeriesButtonBackground.width
+    m.recordSeriesOutline.height = recordSeriesButtonBackground.height
+
+    m.userCanRecord = m.global.session.user.settings["livetv.canrecord"]
+    if m.userCanRecord = false
+        m.recordButton.visible = false
+        m.recordSeriesButton.visible = false
+    end if
+end sub
+
+sub updateLabels(recordText = "Record", recordSeriesText = "Record Series")
+    m.recordLabel.text = tr(recordText)
+    m.recordSeriesLabel.text = tr(recordSeriesText)
+
+    boundingRect = m.recordButton.boundingRect()
+    recordButtonBackground = m.top.findNode("recordButtonBackground")
+    recordButtonBackground.width = boundingRect.width
+    recordButtonBackground.height = boundingRect.height
+    m.recordOutline.width = recordButtonBackground.width
+    m.recordOutline.height = recordButtonBackground.height
+
+    boundingRect = m.recordSeriesButton.boundingRect()
+    recordSeriesButtonBackground = m.top.findNode("recordSeriesButtonBackground")
+    recordSeriesButtonBackground.width = boundingRect.width
+    recordSeriesButtonBackground.height = boundingRect.height
+    m.recordSeriesOutline.width = recordSeriesButtonBackground.width
+    m.recordSeriesOutline.height = recordSeriesButtonBackground.height
+end sub
+
+sub channelUpdated()
+    if m.top.channel = invalid
+        m.top.findNode("noInfoChannelName").text = ""
+        m.channelName.text = ""
+    else
+        m.top.findNode("noInfoChannelName").text = m.top.channel.Title
+        m.channelName.text = m.top.channel.Title
+        if m.top.programDetails = invalid
+            m.image.uri = m.top.channel.posterURL
+        end if
+        m.favorite.visible = m.top.channel.favorite
+    end if
+end sub
+
+sub programUpdated()
+
+    m.top.watchSelectedChannel = false
+    m.top.recordSelectedChannel = false
+    m.top.recordSeriesSelectedChannel = false
+    m.overview.maxLines = m.maxDetailLines
+    prog = m.top.programDetails
+
+    ' If no program selected, hide details view
+    if prog = invalid
+        channelUpdated()
+        m.detailsView.visible = "false"
+        m.noInfoView.visible = "true"
+        return
+    end if
+
+    m.programName.text = prog.Title
+    m.overview.text = prog.description
+
+    m.episodeDetailsGroup.removeChildrenIndex(m.episodeDetailsGroup.getChildCount(), 0)
+
+    if prog.isLive
+        m.episodeDetailsGroup.appendChild(m.isLiveGroup)
+    else if prog.isRepeat
+        m.episodeDetailsGroup.appendChild(m.isRepeatGroup)
+    end if
+
+    ' Episode Number
+    if prog.seasonNumber > 0 and prog.episodeNumber > 0
+        m.episodeNumber.text = "S" + StrI(prog.seasonNumber).trim() + ":E" + StrI(prog.episodeNumber).trim()
+        if prog.episodeTitle <> "" then m.episodeNumber.text = m.episodeNumber.text + " -" ' Add a Dash if showing Episode Number and Title
+        m.episodeDetailsGroup.appendChild(m.episodeNumber)
+    end if
+
+    if prog.episodeTitle <> invalid and prog.episodeTitle <> ""
+        m.episodeTitle.text = prog.episodeTitle
+        m.episodeTitle.visible = true
+        m.episodeDetailsGroup.appendChild(m.episodeTitle)
+    end if
+
+    m.duration.text = getDurationStringFromSeconds(prog.PlayDuration)
+
+    ' Calculate Broadcast Details
+    now = createObject("roDateTime")
+    startDate = createObject("roDateTime")
+    endDate = createObject("roDateTime")
+    startDate.FromISO8601String(prog.StartDate)
+    endDate.FromISO8601String(prog.EndDate)
+
+    day = getRelativeDayName(startDate)
+
+    ' Get Start Date in local timezone for display to user
+    localStartDate = createObject("roDateTime")
+    localStartDate.FromISO8601String(prog.StartDate)
+    localStartDate.ToLocalTime()
+
+    if startDate.AsSeconds() < now.AsSeconds() and endDate.AsSeconds() > now.AsSeconds()
+        if day = "today"
+            m.broadcastDetails.text = tr("Started at") + " " + formatTime(localStartDate)
+        else
+            m.broadcastDetails.text = tr("Started") + " " + tr(day) + ", " + formatTime(localStartDate)
+        end if
+    else if startDate.AsSeconds() > now.AsSeconds()
+        if day = "today"
+            m.broadcastDetails.text = tr("Starts at") + " " + formatTime(localStartDate)
+        else
+            m.broadcastDetails.text = tr("Starts") + " " + tr(day) + ", " + formatTime(localStartDate)
+        end if
+    else
+        if day = "today"
+            m.broadcastDetails.text = tr("Ended at") + " " + formatTime(localStartDate)
+        else
+            m.broadcastDetails.text = tr("Ended") + " " + tr(day) + ", " + formatTime(localStartDate)
+        end if
+    end if
+
+    m.image.uri = prog.PosterURL
+
+    ' If currently being recorded, change button to "Stop Recording"
+    if prog.json.TimerId <> invalid
+        if prog.json.SeriesTimerId <> invalid
+            updateLabels("Cancel Recording", "Cancel Series Recording")
+        else
+            updateLabels("Cancel Recording", "Record Series")
+        end if
+    else
+        updateLabels()
+    end if
+
+    ' If not a series, hide Record Series button
+    if prog.json.isSeries <> true ' could be invalid or false
+        m.recordSeriesButton.visible = false
+    else
+        m.recordSeriesButton.visible = true
+    end if
+
+    m.detailsView.visible = "true"
+    m.noInfoView.visible = "false"
+
+    m.top.height = m.detailsView.boundingRect().height
+    m.overview.maxLines = m.maxPreviewLines
+end sub
+
+'
+' Get relative date name for a date (yesterday, today, tomorrow, or otherwise weekday name )
+function getRelativeDayName(date) as string
+
+    now = createObject("roDateTime")
+
+    ' Check for Today
+    if now.AsDateString("short-date-dashes") = date.AsDateString("short-date-dashes")
+        return "today"
+    end if
+
+    ' Check for Yesterday
+    todayMidnight = now.AsSeconds() - (now.AsSeconds() mod 86400)
+    dateMidnight = date.AsSeconds() - (date.AsSeconds() mod 86400)
+
+    if todayMidnight - dateMidnight = 86400
+        return "yesterday"
+    end if
+
+    if dateMidnight - todayMidnight = 86400
+        return "tomorrow"
+    end if
+
+    return date.GetWeekday()
+
+end function
+
+'
+' Get program duration string (e.g. 1h 20m)
+function getDurationStringFromSeconds(seconds) as string
+
+    hours = 0
+    minutes = seconds / 60.0
+
+    if minutes > 60
+        hours = (minutes - (minutes mod 60)) / 60
+        minutes = minutes mod 60
+    end if
+
+    if hours > 0
+        return "%1h %2m".Replace("%1", StrI(hours).trim()).Replace("%2", StrI(minutes).trim())
+    else
+        return "%1m".Replace("%1", StrI(minutes).trim())
+    end if
+
+end function
+
+'
+' Show view channel button when item has Focus
+sub focusChanged()
+    if m.top.hasFocus = true
+        m.overview.maxLines = m.maxDetailLines
+        m.viewChannelFocusAnimationOpacity.keyValue = [0, 1]
+        m.recordFocusAnimationOpacity.keyValue = [0, 1]
+        m.recordSeriesFocusAnimationOpacity.keyValue = [0, 1]
+        m.viewChannelButton.setFocus(true)
+        m.viewChannelOutline.visible = true
+        m.recordOutline.visible = false
+        m.recordSeriesOutline.visible = false
+        m.viewChannelButtonBackground.blendColor = "#006fab"
+        m.recordButtonBackground.blendColor = "#000000"
+        m.recordSeriesButtonBackground.blendColor = "#000000"
+    else
+        m.top.watchSelectedChannel = false
+        m.top.recordSelectedChannel = false
+        m.top.recordSeriesSelectedChannel = false
+        m.viewChannelFocusAnimationOpacity.keyValue = [1, 0]
+        m.recordFocusAnimationOpacity.keyValue = [1, 0]
+        m.recordSeriesFocusAnimationOpacity.keyValue = [1, 0]
+    end if
+
+    m.focusAnimation.control = "start"
+
+end sub
+
+sub onAnimationComplete()
+    if m.focusAnimation.state = "stopped" and m.top.hasFocus = false
+        m.overview.maxLines = m.maxPreviewLines
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "OK" and m.viewChannelButton.hasFocus()
+        m.top.watchSelectedChannel = true
+        return true
+    else if key = "OK" and m.recordButton.hasFocus()
+        m.top.recordSelectedChannel = true
+        return true
+    else if key = "OK" and m.recordSeriesButton.hasFocus()
+        m.top.recordSeriesSelectedChannel = true
+        return true
+    end if
+
+    if m.userCanRecord = true
+        if key = "right" and m.viewChannelButton.hasFocus()
+            m.recordButton.setFocus(true)
+            m.viewChannelOutline.visible = false
+            m.recordOutline.visible = true
+            m.viewChannelButtonBackground.blendColor = "#000000"
+            m.recordButtonBackground.blendColor = "#006fab"
+            m.recordSeriesButtonBackground.blendColor = "#000000"
+            return true
+        else if key = "right" and m.recordButton.hasFocus()
+            m.recordSeriesButton.setFocus(true)
+            m.recordOutline.visible = false
+            m.recordSeriesOutline.visible = true
+            m.viewChannelButtonBackground.blendColor = "#000000"
+            m.recordButtonBackground.blendColor = "#000000"
+            m.recordSeriesButtonBackground.blendColor = "#006fab"
+            return true
+        else if key = "left" and m.recordSeriesButton.hasFocus()
+            m.recordButton.setFocus(true)
+            m.recordOutline.visible = true
+            m.recordSeriesOutline.visible = false
+            m.viewChannelButtonBackground.blendColor = "#000000"
+            m.recordButtonBackground.blendColor = "#006fab"
+            m.recordSeriesButtonBackground.blendColor = "#000000"
+            return true
+        else if key = "left" and m.recordButton.hasFocus()
+            m.viewChannelButton.setFocus(true)
+            m.viewChannelOutline.visible = true
+            m.recordOutline.visible = false
+            m.viewChannelButtonBackground.blendColor = "#006fab"
+            m.recordButtonBackground.blendColor = "#000000"
+            m.recordSeriesButtonBackground.blendColor = "#000000"
+            return true
+        end if
+    end if
+
+    if key = "up" or key = "down"
+        return true
+    end if
+
+    return false
+end function
+
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_RecordProgramTask.bs.html b/docs/api/components_liveTv_RecordProgramTask.bs.html new file mode 100644 index 000000000..6f8b1051e --- /dev/null +++ b/docs/api/components_liveTv_RecordProgramTask.bs.html @@ -0,0 +1,63 @@ +Source: components/liveTv/RecordProgramTask.bs
On this page

components_liveTv_RecordProgramTask.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/roku_modules/log/LogMixin.brs"
+
+sub init()
+    m.log = log.Logger("RecordProgramTask")
+    m.top.functionName = "RecordOrCancelProgram"
+end sub
+
+sub RecordOrCancelProgram()
+    if m.top.programDetails <> invalid
+        ' Are we setting up a recording or canceling one?
+        TimerId = invalid
+        if m.top.programDetails.json.TimerId <> invalid and m.top.programDetails.json.TimerId <> ""
+            TimerId = m.top.programDetails.json.TimerId
+        end if
+
+        if TimerId = invalid
+            ' Setting up a recording...
+            programId = m.top.programDetails.Id
+
+            ' Get Live TV default params from server...
+            url = "LiveTv/Timers/Defaults"
+            params = {
+                programId: programId
+            }
+
+            resp = APIRequest(url, params)
+            data = getJson(resp)
+
+            if data <> invalid
+                ' Create recording timer...
+                if m.top.recordSeries = true
+                    url = "LiveTv/SeriesTimers"
+                else
+                    url = "LiveTv/Timers"
+                end if
+                resp = APIRequest(url)
+                postJson(resp, FormatJson(data))
+                m.top.programDetails.hdSmallIconUrl = "pkg:/images/red.png"
+            else
+                ' Error msg to user?
+                m.log.error("Could not retrieve live TV Defaults from Server")
+            end if
+        else
+            ' Cancelling a recording...
+            if m.top.recordSeries = true
+                TimerId = m.top.programDetails.json.SeriesTimerId
+                url = Substitute("LiveTv/SeriesTimers/{0}", TimerId)
+            else
+                url = Substitute("LiveTv/Timers/{0}", TimerId)
+            end if
+            resp = APIRequest(url)
+            deleteVoid(resp)
+            m.top.programDetails.hdSmallIconUrl = invalid
+        end if
+    end if
+
+    m.top.recordOperationDone = true
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_liveTv_schedule.bs.html b/docs/api/components_liveTv_schedule.bs.html new file mode 100644 index 000000000..86ef65ef4 --- /dev/null +++ b/docs/api/components_liveTv_schedule.bs.html @@ -0,0 +1,308 @@ +Source: components/liveTv/schedule.bs
On this page

components_liveTv_schedule.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.EPGLaunchCompleteSignaled = false
+    m.scheduleGrid = m.top.findNode("scheduleGrid")
+    m.detailsPane = m.top.findNode("detailsPane")
+
+    m.detailsPane.observeField("watchSelectedChannel", "onWatchChannelSelected")
+    m.detailsPane.observeField("recordSelectedChannel", "onRecordChannelSelected")
+    m.detailsPane.observeField("recordSeriesSelectedChannel", "onRecordSeriesChannelSelected")
+    m.gridStartDate = CreateObject("roDateTime")
+    m.scheduleGrid.contentStartTime = m.gridStartDate.AsSeconds() - 1800
+    m.gridEndDate = createObject("roDateTime")
+    m.gridEndDate.FromSeconds(m.gridStartDate.AsSeconds() + (24 * 60 * 60))
+
+    m.scheduleGrid.observeField("programFocused", "onProgramFocused")
+    m.scheduleGrid.observeField("programSelected", "onProgramSelected")
+    m.scheduleGrid.observeField("leftEdgeTargetTime", "onGridScrolled")
+    m.scheduleGrid.channelInfoWidth = 350
+
+    m.gridMoveAnimation = m.top.findNode("gridMoveAnimation")
+    m.gridMoveAnimationPosition = m.top.findNode("gridMoveAnimationPosition")
+
+    m.LoadChannelsTask = createObject("roSGNode", "LoadChannelsTask")
+    m.LoadChannelsTask.observeField("channels", "onChannelsLoaded")
+    m.LoadChannelsTask.control = "RUN"
+
+    m.top.lastFocus = m.scheduleGrid
+
+    m.channelIndex = {}
+end sub
+
+sub channelFilterSet()
+    m.scheduleGrid.jumpToChannel = 0
+    if m.top.filter <> invalid and m.LoadChannelsTask.filter <> m.top.filter
+        if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop"
+
+        m.LoadChannelsTask.filter = m.top.filter
+        m.LoadChannelsTask.control = "RUN"
+    end if
+
+end sub
+
+'Voice Search set
+sub channelsearchTermSet()
+    m.scheduleGrid.jumpToChannel = 0
+    'Reset filter if user says all
+    if LCase(m.top.searchTerm) = LCase(tr("all")) or m.LoadChannelsTask.searchTerm = LCase(tr("all"))
+        m.top.searchTerm = " "
+        m.LoadChannelsTask.searchTerm = " "
+        startLoadingSpinner()
+        m.LoadChannelsTask.control = "RUN"
+        'filter if the searterm is not invalid
+    else if m.top.searchTerm <> invalid and LCase(m.LoadChannelsTask.searchTerm) <> LCase(m.top.searchTerm)
+        if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop"
+
+        m.LoadChannelsTask.searchTerm = m.top.searchTerm
+        startLoadingSpinner()
+        m.LoadChannelsTask.control = "RUN"
+    end if
+
+end sub
+
+' Initial list of channels loaded
+sub onChannelsLoaded()
+    gridData = createObject("roSGNode", "ContentNode")
+
+    counter = 0
+    channelIdList = ""
+
+    'if search returns channels
+    if m.LoadChannelsTask.channels.count() > 0
+        for each item in m.LoadChannelsTask.channels
+            gridData.appendChild(item)
+            m.channelIndex[item.Id] = counter
+            counter = counter + 1
+            channelIdList = channelIdList + item.Id + ","
+        end for
+        m.scheduleGrid.content = gridData
+
+        m.LoadScheduleTask = createObject("roSGNode", "LoadScheduleTask")
+        m.LoadScheduleTask.observeField("schedule", "onScheduleLoaded")
+
+        m.LoadScheduleTask.startTime = m.gridStartDate.ToISOString()
+        m.LoadScheduleTask.endTime = m.gridEndDate.ToISOString()
+        m.LoadScheduleTask.channelIds = channelIdList
+        m.LoadScheduleTask.control = "RUN"
+
+        m.LoadProgramDetailsTask = createObject("roSGNode", "LoadProgramDetailsTask")
+        m.LoadProgramDetailsTask.observeField("programDetails", "onProgramDetailsLoaded")
+
+        m.scheduleGrid.setFocus(true)
+        if m.EPGLaunchCompleteSignaled = false
+            m.top.signalBeacon("EPGLaunchComplete") ' Required Roku Performance monitoring
+            m.EPGLaunchCompleteSignaled = true
+        end if
+        m.LoadChannelsTask.channels = []
+
+    end if
+
+end sub
+
+' When LoadScheduleTask completes (initial or more data) and we have a schedule to display
+sub onScheduleLoaded()
+
+    ' make sure we actually have a schedule (i.e. filter by favorites, but no channels have been favorited)
+    if m.scheduleGrid.content.GetChildCount() <= 0
+        return
+    end if
+
+    for each item in m.LoadScheduleTask.schedule
+
+        channel = m.scheduleGrid.content.GetChild(m.channelIndex[item.ChannelId])
+
+        if channel.PosterUrl <> ""
+            item.channelLogoUri = channel.PosterUrl
+        end if
+        if channel.Title <> ""
+            item.channelName = channel.Title
+        end if
+
+        channel.appendChild(item)
+    end for
+
+    m.scheduleGrid.showLoadingDataFeedback = false
+    m.scheduleGrid.setFocus(true)
+    m.LoadScheduleTask.schedule = []
+    stopLoadingSpinner()
+end sub
+
+sub onProgramFocused()
+    m.top.watchChannel = invalid
+
+    ' Make sure we have channels (i.e. filter set to favorite yet there are none)
+    if m.scheduleGrid.content.getChildCount() <= 0
+        channel = invalid
+    else
+        channel = m.scheduleGrid.content.GetChild(m.scheduleGrid.programFocusedDetails.focusChannelIndex)
+    end if
+
+    m.detailsPane.channel = channel
+    m.top.focusedChannel = channel
+
+    ' Exit if Channels not yet loaded
+
+    if channel = invalid or channel.getChildCount() = 0
+
+        m.detailsPane.programDetails = invalid
+        return
+    end if
+
+    prog = channel.GetChild(m.scheduleGrid.programFocusedDetails.focusIndex)
+
+    if prog <> invalid and prog.fullyLoaded = false
+        m.LoadProgramDetailsTask.programId = prog.Id
+        m.LoadProgramDetailsTask.channelIndex = m.scheduleGrid.programFocusedDetails.focusChannelIndex
+        m.LoadProgramDetailsTask.programIndex = m.scheduleGrid.programFocusedDetails.focusIndex
+        m.LoadProgramDetailsTask.control = "RUN"
+    end if
+
+    m.detailsPane.programDetails = prog
+end sub
+
+' Update the Program Details with full information
+sub onProgramDetailsLoaded()
+    if m.LoadProgramDetailsTask.programDetails = invalid then return
+    channel = m.scheduleGrid.content.GetChild(m.LoadProgramDetailsTask.programDetails.channelIndex)
+
+    ' If TV Show does not have its own image, use the channel logo
+    if m.LoadProgramDetailsTask.programDetails.PosterUrl = invalid or m.LoadProgramDetailsTask.programDetails.PosterUrl = ""
+        m.LoadProgramDetailsTask.programDetails.PosterUrl = channel.PosterUrl
+    end if
+
+    channel.ReplaceChild(m.LoadProgramDetailsTask.programDetails, m.LoadProgramDetailsTask.programDetails.programIndex)
+    m.LoadProgramDetailsTask.programDetails = invalid
+    m.scheduleGrid.showLoadingDataFeedback = false
+end sub
+
+
+sub onProgramSelected()
+    ' If there is no program data - view the channel
+    if m.detailsPane.programDetails = invalid
+        m.top.watchChannel = m.scheduleGrid.content.GetChild(m.scheduleGrid.programFocusedDetails.focusChannelIndex)
+        return
+    end if
+
+    ' Move Grid Down
+    focusProgramDetails(true)
+end sub
+
+' Move the TV Guide Grid down or up depending whether details are selected
+sub focusProgramDetails(setFocused)
+
+    h = m.detailsPane.height
+    if h < 400 then h = 400
+    h = h + 160 + 80
+
+    if setFocused = true
+        m.gridMoveAnimationPosition.keyValue = [[0, 600], [0, h]]
+        m.detailsPane.setFocus(true)
+        m.detailsPane.hasFocus = true
+        m.top.lastFocus = m.detailsPane
+    else
+        m.detailsPane.hasFocus = false
+        m.gridMoveAnimationPosition.keyValue = [[0, h], [0, 600]]
+        m.scheduleGrid.setFocus(true)
+        m.top.lastFocus = m.scheduleGrid
+    end if
+
+    m.gridMoveAnimation.control = "start"
+end sub
+
+' Handle user selecting "Watch Channel" from Program Details
+sub onWatchChannelSelected()
+
+    if m.detailsPane.watchSelectedChannel = false then return
+
+    ' Set focus back to grid before showing channel, to ensure grid has focus when we return
+    focusProgramDetails(false)
+
+    m.top.watchChannel = m.detailsPane.channel
+end sub
+
+' As user scrolls grid, check if more data requries to be loaded
+sub onGridScrolled()
+
+    ' If we're within 12 hours of end of grid, load next 24hrs of data
+    if m.scheduleGrid.leftEdgeTargetTime + (12 * 60 * 60) > m.gridEndDate.AsSeconds()
+
+        ' Ensure the task is not already (still) running,
+        if m.LoadScheduleTask.state <> "run"
+            m.LoadScheduleTask.startTime = m.gridEndDate.ToISOString()
+            m.gridEndDate.FromSeconds(m.gridEndDate.AsSeconds() + (24 * 60 * 60))
+            m.LoadScheduleTask.endTime = m.gridEndDate.ToISOString()
+            m.LoadScheduleTask.control = "RUN"
+        end if
+    end if
+end sub
+
+' Handle user selecting "Record Channel" from Program Details
+sub onRecordChannelSelected()
+    if m.detailsPane.recordSelectedChannel = false then return
+
+    ' Set focus back to grid before showing channel, to ensure grid has focus when we return
+    focusProgramDetails(false)
+
+    m.scheduleGrid.showLoadingDataFeedback = true
+
+    m.RecordProgramTask = createObject("roSGNode", "RecordProgramTask")
+    m.RecordProgramTask.programDetails = m.detailsPane.programDetails
+    m.RecordProgramTask.recordSeries = false
+    m.RecordProgramTask.observeField("recordOperationDone", "onRecordOperationDone")
+    m.RecordProgramTask.control = "RUN"
+end sub
+
+' Handle user selecting "Record Series" from Program Details
+sub onRecordSeriesChannelSelected()
+    if m.detailsPane.recordSeriesSelectedChannel = false then return
+
+    ' Set focus back to grid before showing channel, to ensure grid has focus when we return
+    focusProgramDetails(false)
+
+    m.scheduleGrid.showLoadingDataFeedback = true
+
+    m.RecordProgramTask = createObject("roSGNode", "RecordProgramTask")
+    m.RecordProgramTask.programDetails = m.detailsPane.programDetails
+    m.RecordProgramTask.recordSeries = true
+    m.RecordProgramTask.observeField("recordOperationDone", "onRecordOperationDone")
+    m.RecordProgramTask.control = "RUN"
+end sub
+
+sub onRecordOperationDone()
+    if m.RecordProgramTask.recordSeries = true and m.LoadScheduleTask.state <> "run"
+        m.LoadScheduleTask.control = "RUN"
+    else
+        ' This reloads just the details for the currently selected program, so that we don't have to
+        ' reload the entire grid...
+        channel = m.scheduleGrid.content.GetChild(m.scheduleGrid.programFocusedDetails.focusChannelIndex)
+        prog = channel.GetChild(m.scheduleGrid.programFocusedDetails.focusIndex)
+        m.LoadProgramDetailsTask.programId = prog.Id
+        m.LoadProgramDetailsTask.channelIndex = m.scheduleGrid.programFocusedDetails.focusChannelIndex
+        m.LoadProgramDetailsTask.programIndex = m.scheduleGrid.programFocusedDetails.focusIndex
+        m.LoadProgramDetailsTask.control = "RUN"
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    detailsGrp = m.top.findNode("detailsPane")
+    gridGrp = m.top.findNode("scheduleGrid")
+
+    if key = "back" and detailsGrp.isInFocusChain()
+        focusProgramDetails(false)
+        detailsGrp.setFocus(false)
+        gridGrp.setFocus(true)
+        return true
+    else if key = "back"
+        m.LoadChannelsTask.control = "stop"
+        m.global.sceneManager.callFunc("popScene")
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_login_UserItem.bs.html b/docs/api/components_login_UserItem.bs.html new file mode 100644 index 000000000..37c7060b3 --- /dev/null +++ b/docs/api/components_login_UserItem.bs.html @@ -0,0 +1,20 @@ +Source: components/login/UserItem.bs
On this page

components_login_UserItem.bs

sub init()
+end sub
+
+sub itemContentChanged()
+    itemData = m.top.itemContent
+    if itemData = invalid then return
+
+    profileImage = m.top.findNode("profileImage")
+    profileName = m.top.findNode("profileName")
+
+    if itemData.imageURL = ""
+        profileImage.uri = "pkg://images/baseline_person_white_48dp.png"
+    else
+        profileImage.uri = itemData.imageURL
+    end if
+    profileName.text = itemData.name
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_login_UserRow.bs.html b/docs/api/components_login_UserRow.bs.html new file mode 100644 index 000000000..3b2424a7d --- /dev/null +++ b/docs/api/components_login_UserRow.bs.html @@ -0,0 +1,56 @@ +Source: components/login/UserRow.bs
On this page

components_login_UserRow.bs

sub init()
+    m.top.itemComponentName = "UserItem"
+    m.top.content = SetData()
+    m.top.observeField("itemSelected", "SetUser")
+    m.top.showRowLabel = [false]
+    updateSize()
+    m.top.setFocus(true)
+end sub
+
+sub updateSize()
+    itemWidth = 300
+    itemHeight = 364
+
+    m.top.visible = true
+
+    ' Size of the individual rows
+    m.top.itemSize = [1660, itemHeight]
+    ' Spacing between Rows
+    m.top.itemSpacing = [0, 40]
+
+    ' Size of items in the row
+    m.top.rowItemSize = [itemWidth, itemHeight]
+    ' Spacing between items in the row
+    m.top.rowItemSpacing = [40, 0]
+end sub
+
+
+function setData()
+    if m.top.itemContent = invalid
+        data = CreateObject("roSGNode", "ContentNode")
+        return data
+    end if
+
+    userData = m.top.itemContent
+    data = CreateObject("roSGNode", "ContentNode")
+    row = data.CreateChild("ContentNode")
+    for each item in userData
+        row.appendChild(item)
+    end for
+    m.top.content = data
+    updateSize()
+    return data
+end function
+
+sub setUser()
+    m.top.userSelected = m.top.itemContent[m.top.rowItemFocused[1]].Name
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_login_UserSelect.bs.html b/docs/api/components_login_UserSelect.bs.html new file mode 100644 index 000000000..c9e5ea2b8 --- /dev/null +++ b/docs/api/components_login_UserSelect.bs.html @@ -0,0 +1,47 @@ +Source: components/login/UserSelect.bs
On this page

components_login_UserSelect.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+end sub
+
+sub itemContentChanged()
+    stopLoadingSpinner()
+    m.top.findNode("UserRow").ItemContent = m.top.itemContent
+    redraw()
+end sub
+
+sub redraw()
+    userCount = m.top.itemContent.Count()
+    topBorder = 360
+    leftBorder = 130
+    itemWidth = 300
+    itemSpacing = 40
+
+    if userCount < 5
+        leftBorder = (1920 - ((userCount * itemWidth) + ((userCount - 1) * itemSpacing))) / 2
+    end if
+    '   break()
+    m.top.findNode("UserRow").translation = [leftBorder, topBorder]
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "back"
+        m.top.backPressed = true
+    else if key = "up"
+        if m.top.focusedChild.isSubType("LabelList")
+            m.top.findNode("UserRow").setFocus(true)
+            return true
+        end if
+    else if key = "down"
+        if m.top.focusedChild.isSubType("UserRow")
+            m.top.findNode("alternateOptions").setFocus(true)
+            return true
+        end if
+    end if
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_manager_QueueManager.bs.html b/docs/api/components_manager_QueueManager.bs.html new file mode 100644 index 000000000..730bba854 --- /dev/null +++ b/docs/api/components_manager_QueueManager.bs.html @@ -0,0 +1,263 @@ +Source: components/manager/QueueManager.bs
On this page

components_manager_QueueManager.bs

import "pkg:/source/utils/misc.bs"
+import "ViewCreator.bs"
+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.hold = []
+    m.queue = []
+    m.originalQueue = []
+    m.queueTypes = []
+    m.isPlaying = false
+    ' Preroll videos only play if user has cinema mode setting enabled
+    m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
+    m.position = 0
+    m.shuffleEnabled = false
+end sub
+
+' Clear all content from play queue
+sub clear()
+    m.isPlaying = false
+    m.queue = []
+    m.queueTypes = []
+    m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
+    setPosition(0)
+end sub
+
+' Clear all hold content
+sub clearHold()
+    m.hold = []
+end sub
+
+' Delete item from play queue at passed index
+sub deleteAtIndex(index)
+    m.queue.Delete(index)
+    m.queueTypes.Delete(index)
+end sub
+
+' Return the number of items in the play queue
+function getCount()
+    return m.queue.count()
+end function
+
+' Return the item currently in focus from the play queue
+function getCurrentItem()
+    return getItemByIndex(m.position)
+end function
+
+' Return the items in the hold
+function getHold()
+    return m.hold
+end function
+
+' Return whether or not shuffle is enabled
+function getIsShuffled()
+    return m.shuffleEnabled
+end function
+
+' Return the item in the passed index from the play queue
+function getItemByIndex(index)
+    return m.queue[index]
+end function
+
+' Returns current playback position within the queue
+function getPosition()
+    return m.position
+end function
+
+' Hold an item
+sub hold(newItem)
+    m.hold.push(newItem)
+end sub
+
+' Move queue position back one
+sub moveBack()
+    m.position--
+end sub
+
+' Move queue position ahead one
+sub moveForward()
+    m.position++
+end sub
+
+' Return the current play queue
+function getQueue()
+    return m.queue
+end function
+
+' Return the types of items in current play queue
+function getQueueTypes()
+    return m.queueTypes
+end function
+
+' Return the unique types of items in current play queue
+function getQueueUniqueTypes()
+    itemTypes = []
+
+    for each item in getQueueTypes()
+        if not inArray(itemTypes, item)
+            itemTypes.push(item)
+        end if
+    end for
+
+    return itemTypes
+end function
+
+' Return item at end of play queue without removing
+function peek()
+    return m.queue.peek()
+end function
+
+' Play items in queue
+sub playQueue()
+    m.isPlaying = true
+    nextItem = getCurrentItem()
+    if not isValid(nextItem) then return
+
+    nextItemMediaType = getItemType(nextItem)
+    if nextItemMediaType = "" then return
+
+    if nextItemMediaType = "audio"
+        CreateAudioPlayerView()
+        return
+    end if
+
+    if nextItemMediaType = "musicvideo"
+        CreateVideoPlayerView()
+        return
+    end if
+
+    if nextItemMediaType = "video"
+        CreateVideoPlayerView()
+        return
+    end if
+
+    if nextItemMediaType = "movie"
+        CreateVideoPlayerView()
+        return
+    end if
+
+    if nextItemMediaType = "episode"
+        CreateVideoPlayerView()
+        return
+    end if
+
+    if nextItemMediaType = "trailer"
+        CreateVideoPlayerView()
+        return
+    end if
+end sub
+
+' Remove item at end of play queue
+sub pop()
+    m.queue.pop()
+    m.queueTypes.pop()
+end sub
+
+' Return isPrerollActive status
+function isPrerollActive() as boolean
+    return m.isPrerollActive
+end function
+
+' Set prerollActive status
+sub setPrerollStatus(newStatus as boolean)
+    m.isPrerollActive = newStatus
+end sub
+
+' Push new items to the play queue
+sub push(newItem)
+    m.queue.push(newItem)
+    m.queueTypes.push(getItemType(newItem))
+end sub
+
+' Set the queue position
+sub setPosition(newPosition)
+    m.position = newPosition
+end sub
+
+' Reset shuffle to off state
+sub resetShuffle()
+    m.shuffleEnabled = false
+end sub
+
+' Toggle shuffleEnabled state
+sub toggleShuffle()
+    m.shuffleEnabled = not m.shuffleEnabled
+
+    if m.shuffleEnabled
+        shuffleQueueItems()
+        return
+    end if
+
+    resetQueueItemOrder()
+end sub
+
+' Reset queue items back to original, unshuffled order
+sub resetQueueItemOrder()
+    set(m.originalQueue)
+end sub
+
+' Return original, unshuffled queue
+function getUnshuffledQueue()
+    return m.originalQueue
+end function
+
+' Save a copy of the original queue and randomize order of queue items
+sub shuffleQueueItems()
+    ' By calling getQueue 2 different ways, Roku avoids needing to do a deep copy
+    m.originalQueue = m.global.queueManager.callFunc("getQueue")
+    itemIDArray = getQueue()
+    temp = invalid
+
+    if m.isPlaying
+        ' Save the currently playing item
+        temp = getCurrentItem()
+        ' remove currently playing item from itemIDArray
+        itemIDArray.Delete(m.position)
+    end if
+
+    ' shuffle all items
+    itemIDArray = shuffleArray(itemIDArray)
+
+    if m.isPlaying
+        ' Put currently playing item in front of itemIDArray
+        itemIDArray.Unshift(temp)
+    end if
+
+    set(itemIDArray)
+end sub
+
+' Return the fitst item in the play queue
+function top()
+    return getItemByIndex(0)
+end function
+
+' Replace play queue with passed array
+sub set(items)
+    clear()
+    m.queue = items
+    for each item in items
+        m.queueTypes.push(getItemType(item))
+    end for
+end sub
+
+' Set starting point for top item in the queue
+sub setTopStartingPoint(positionTicks)
+    m.queue[0].startingPoint = positionTicks
+end sub
+
+function getItemType(item) as string
+    if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
+        return LCase(item.json.mediatype)
+    else if isValid(item) and isValid(item.type) and item.type <> ""
+        return LCase(item.type)
+    end if
+
+    return ""
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_manager_ViewCreator.bs.html b/docs/api/components_manager_ViewCreator.bs.html new file mode 100644 index 000000000..f0c1a2401 --- /dev/null +++ b/docs/api/components_manager_ViewCreator.bs.html @@ -0,0 +1,191 @@ +Source: components/manager/ViewCreator.bs
On this page

components_manager_ViewCreator.bs

' Play Audio
+sub CreateAudioPlayerView()
+    m.view = CreateObject("roSGNode", "AudioPlayerView")
+    m.view.observeField("state", "onStateChange")
+    m.global.sceneManager.callFunc("pushScene", m.view)
+end sub
+
+' Play Video
+sub CreateVideoPlayerView()
+    m.playbackData = {}
+    m.selectedSubtitle = {}
+
+    m.view = CreateObject("roSGNode", "VideoPlayerView")
+    m.view.observeField("state", "onStateChange")
+    m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
+    m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
+
+    mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").mediaSourceId
+
+    if not isValid(mediaSourceId) or mediaSourceId = ""
+        mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").id
+    end if
+
+    m.getPlaybackInfoTask = createObject("roSGNode", "GetPlaybackInfoTask")
+    m.getPlaybackInfoTask.videoID = mediaSourceId
+    m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
+
+    m.global.sceneManager.callFunc("pushScene", m.view)
+end sub
+
+' -----------------
+' Event Handlers
+' -----------------
+
+' User requested subtitle selection popup
+sub onSelectSubtitlePressed()
+    ' None is always first in the subtitle list
+    subtitleData = {
+        data: [{
+            "Index": -1,
+            "IsExternal": false,
+            "Track": {
+                "description": "None"
+            },
+            "Type": "subtitleselection"
+        }]
+    }
+
+    for each item in m.view.fullSubtitleData
+        item.type = "subtitleselection"
+
+        if m.view.selectedSubtitle <> -1
+            ' Subtitle is a track within the file
+            if item.index = m.view.selectedSubtitle
+                item.selected = true
+            end if
+        else
+            ' Subtitle is from an external source
+            availableSubtitleTrackIndex = availSubtitleTrackIdx(item.track.TrackName)
+            if availableSubtitleTrackIndex <> -1
+
+                ' Convert Jellyfin subtitle track name to Roku track name
+                subtitleFullTrackName = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
+
+                if subtitleFullTrackName = m.view.subtitleTrack
+                    item.selected = true
+                end if
+
+            end if
+        end if
+
+        subtitleData.data.push(item)
+    end for
+
+    m.global.sceneManager.callFunc("radioDialog", tr("Select Subtitles"), subtitleData)
+    m.global.sceneManager.observeField("returnData", "onSelectionMade")
+end sub
+
+' User has selected something from the radioDialog popup
+sub onSelectionMade()
+    m.global.sceneManager.unobserveField("returnData")
+
+    if not isValid(m.global.sceneManager.returnData) then return
+    if not isValid(m.global.sceneManager.returnData.type) then return
+
+    if LCase(m.global.sceneManager.returnData.type) = "subtitleselection"
+        processSubtitleSelection()
+    end if
+end sub
+
+sub processSubtitleSelection()
+    m.selectedSubtitle = m.global.sceneManager.returnData
+
+    ' The selected encoded subtitle did not change.
+    if m.view.selectedSubtitle <> -1 or m.selectedSubtitle.index <> -1
+        if m.view.selectedSubtitle = m.selectedSubtitle.index then return
+    end if
+
+    ' The playbackData is now outdated and must be refreshed
+    m.playbackData = invalid
+
+    if LCase(m.selectedSubtitle.track.description) = "none"
+        m.view.globalCaptionMode = "Off"
+        m.view.subtitleTrack = ""
+
+        if m.view.selectedSubtitle <> -1
+            m.view.selectedSubtitle = -1
+        end if
+
+        return
+    end if
+
+    if m.selectedSubtitle.IsEncoded
+        m.view.globalCaptionMode = "Off"
+    else
+        m.view.globalCaptionMode = "On"
+    end if
+
+    if m.selectedSubtitle.IsExternal
+        availableSubtitleTrackIndex = availSubtitleTrackIdx(m.selectedSubtitle.Track.TrackName)
+        if availableSubtitleTrackIndex = -1 then return
+
+        m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
+    else
+        m.view.selectedSubtitle = m.selectedSubtitle.Index
+    end if
+end sub
+
+' User requested playback info
+sub onSelectPlaybackInfoPressed()
+    ' Check if we already have playback info and show it in a popup
+    if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
+        m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
+        return
+    end if
+
+    m.getPlaybackInfoTask.control = "RUN"
+end sub
+
+' The playback info task has returned data
+sub onPlaybackInfoLoaded()
+    m.playbackData = m.getPlaybackInfoTask.data
+
+    ' Check if we have playback info and show it in a popup
+    if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
+        m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
+    end if
+end sub
+
+' Playback state change event handlers
+sub onStateChange()
+    if LCase(m.view.state) = "finished"
+        ' Close any open dialogs
+        if m.global.sceneManager.callFunc("isDialogOpen")
+            m.global.sceneManager.callFunc("dismissDialog")
+        end if
+
+        ' If there is something next in the queue, play it
+        if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
+            m.global.sceneManager.callFunc("clearPreviousScene")
+            m.global.queueManager.callFunc("moveForward")
+            m.global.queueManager.callFunc("playQueue")
+            return
+        end if
+
+        ' Playback completed, return user to previous screen
+        m.global.sceneManager.callFunc("popScene")
+        m.global.audioPlayer.loopMode = ""
+    end if
+end sub
+
+' 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(tracknameToFind as string) as integer
+    idx = 0
+    for each availTrack in m.view.availableSubtitleTracks
+        ' The TrackName must contain the URL we supplied originally, though
+        ' Roku mangles the name a bit, so we check if the URL is a substring, rather
+        ' than strict equality
+        if Instr(1, availTrack.TrackName, tracknameToFind)
+            return idx
+        end if
+        idx = idx + 1
+    end for
+    return -1
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_mediaPlayers_AudioPlayer.bs.html b/docs/api/components_mediaPlayers_AudioPlayer.bs.html new file mode 100644 index 000000000..1f8085caa --- /dev/null +++ b/docs/api/components_mediaPlayers_AudioPlayer.bs.html @@ -0,0 +1,46 @@ +Source: components/mediaPlayers/AudioPlayer.bs
On this page

components_mediaPlayers_AudioPlayer.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.playReported = false
+    m.top.observeField("state", "audioStateChanged")
+end sub
+
+' State Change Event Handler
+sub audioStateChanged()
+    currentState = LCase(m.top.state)
+
+    reportedPlaybackState = "update"
+
+    m.top.disableScreenSaver = (currentState = "playing")
+
+    if currentState = "playing" and not m.playReported
+        reportedPlaybackState = "start"
+        m.playReported = true
+    else if currentState = "stopped" or currentState = "finished"
+        reportedPlaybackState = "stop"
+        m.playReported = false
+    end if
+
+    ReportPlayback(reportedPlaybackState)
+end sub
+
+' Report playback to server
+sub ReportPlayback(state as string)
+
+    if not isValid(m.top.position) then return
+
+    params = {
+        "ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
+        "PlaySessionId": m.top.content.id,
+        "PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
+        "IsPaused": (LCase(m.top.state) = "paused")
+    }
+
+    ' Report playstate via global task
+    playstateTask = m.global.playstateTask
+    playstateTask.setFields({ status: state, params: params })
+    playstateTask.control = "RUN"
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_movies_AudioTrackListItem.bs.html b/docs/api/components_movies_AudioTrackListItem.bs.html new file mode 100644 index 000000000..bf69bfd34 --- /dev/null +++ b/docs/api/components_movies_AudioTrackListItem.bs.html @@ -0,0 +1,36 @@ +Source: components/movies/AudioTrackListItem.bs
On this page

components_movies_AudioTrackListItem.bs

sub init()
+    m.title = m.top.findNode("title")
+    m.description = m.top.findNode("description")
+    m.selectedIcon = m.top.findNode("selectedIcon")
+end sub
+
+sub itemContentChanged()
+    m.title.text = m.top.itemContent.title
+    m.description.text = m.top.itemContent.description
+
+    if m.top.itemContent.description = ""
+        m.title.translation = [50, 20]
+    end if
+
+    if m.top.itemContent.selected
+        m.selectedIcon.uri = m.global.constants.icons.check_white
+    else
+        m.selectedIcon.uri = ""
+    end if
+
+end sub
+
+'
+'Scroll description if focused
+sub focusChanged()
+
+    if m.top.itemHasFocus = true
+        m.description.repeatCount = -1
+    else
+        m.description.repeatCount = 0
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_movies_MovieDetails.bs.html b/docs/api/components_movies_MovieDetails.bs.html new file mode 100644 index 000000000..e9f35d7cf --- /dev/null +++ b/docs/api/components_movies_MovieDetails.bs.html @@ -0,0 +1,397 @@ +Source: components/movies/MovieDetails.bs
On this page

components_movies_MovieDetails.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.extrasGrp = m.top.findnode("extrasGrp")
+    m.extrasGrid = m.top.findNode("extrasGrid")
+    m.top.optionsAvailable = false
+
+    m.options = m.top.findNode("movieOptions")
+    m.infoGroup = m.top.findNode("infoGroup")
+
+    main = m.top.findNode("main_group")
+    main.translation = [96, 175]
+    overview = m.top.findNode("overview")
+    overview.width = 1920 - 96 - 300 - 96 - 30
+
+    m.details = m.top.findNode("details")
+    m.tagline = m.top.findNode("tagline")
+
+    m.buttonGrp = m.top.findNode("buttons")
+    m.buttonGrp.setFocus(true)
+    m.top.lastFocus = m.buttonGrp
+
+    m.top.observeField("itemContent", "itemContentChanged")
+end sub
+
+sub OnScreenShown()
+    ' set focus to button group
+    if m.extrasGrp.opacity = 1
+        m.top.lastFocus.setFocus(true)
+    else
+        m.buttonGrp.setFocus(true)
+    end if
+end sub
+
+sub trailerAvailableChanged()
+    if m.top.trailerAvailable
+        ' add trailor button to button group
+        trailerButton = CreateObject("roSGNode", "Button")
+        trailerButton.id = "trailer-button"
+        trailerButton.text = tr("Play Trailer")
+        trailerButton.iconUri = ""
+        trailerButton.focusedIconUri = ""
+        trailerButton.maxWidth = "300"
+        trailerButton.minWidth = "280"
+        m.buttonGrp.appendChild(trailerButton)
+    else
+        ' remove trailor button from button group
+        m.buttonGrp.removeChild(m.top.findNode("trailer-button"))
+    end if
+end sub
+
+sub itemContentChanged()
+    ' Updates video metadata
+    item = m.top.itemContent
+    itemData = item.json
+    m.top.id = itemData.id
+    m.top.findNode("moviePoster").uri = m.top.itemContent.posterURL
+
+    ' Set default video source if user hasn't selected one yet
+    if m.top.selectedVideoStreamId = "" and isValid(itemData.MediaSources)
+        m.top.selectedVideoStreamId = itemData.MediaSources[0].id
+    end if
+
+    ' Find first Audio Stream and set that as default
+    SetDefaultAudioTrack(itemData)
+
+    ' Handle all "As Is" fields
+    m.top.overhangTitle = itemData.name
+    setFieldText("releaseYear", itemData.productionYear)
+    setFieldText("overview", itemData.overview)
+
+    if itemData.officialRating <> invalid
+        setFieldText("officialRating", itemData.officialRating)
+    else
+        m.infoGroup.removeChild(m.top.findNode("officialRating"))
+    end if
+
+    if itemData.communityRating <> invalid
+        setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
+        m.top.findNode("communityRatingGroup").visible = "true"
+    else
+        ' hide the star icon
+        m.infoGroup.removeChild(m.top.findNode("communityRatingGroup"))
+    end if
+
+    if itemData.CriticRating <> invalid
+        setFieldText("criticRatingLabel", itemData.criticRating)
+        if itemData.CriticRating > 60
+            tomato = "pkg:/images/fresh.png"
+        else
+            tomato = "pkg:/images/rotten.png"
+        end if
+        m.top.findNode("criticRatingIcon").uri = tomato
+    else
+        m.infoGroup.removeChild(m.top.findNode("criticRatingGroup"))
+    end if
+
+    if type(itemData.RunTimeTicks) = "LongInteger"
+        setFieldText("runtime", stri(getRuntime()) + " mins")
+        if m.global.session.user.settings["ui.design.hideclock"] <> true
+            setFieldText("ends-at", tr("Ends at %1").Replace("%1", getEndTime()))
+        end if
+    end if
+
+    if itemData.genres.count() > 0
+        setFieldText("genres", tr("Genres") + ": " + itemData.genres.join(", "))
+    else
+        m.top.findNode("details").removeChild(m.top.findNode("genres"))
+    end if
+
+    ' show tags if there are no genres to display
+    if itemData.genres.count() = 0 and isValid(itemData.tags) and itemData.tags.count() > 0
+        setFieldText("genres", tr("Tags") + ": " + itemData.tags.join(", "))
+    end if
+
+    directors = []
+    for each person in itemData.people
+        if person.type = "Director"
+            directors.push(person.name)
+        end if
+    end for
+    if directors.count() > 0
+        setFieldText("director", tr("Director") + ": " + directors.join(", "))
+    else
+        m.top.findNode("details").removeChild(m.top.findNode("director"))
+    end if
+
+    if m.global.session.user.settings["ui.details.hidetagline"] = false
+        if itemData.taglines.count() > 0
+            setFieldText("tagline", itemData.taglines[0])
+        end if
+    else
+        m.details.removeChild(m.tagline)
+    end if
+
+    'set aired date if type is Episode
+    if itemData.PremiereDate <> invalid and itemData.Type = "Episode"
+        airDate = CreateObject("roDateTime")
+        airDate.FromISO8601String(itemData.PremiereDate)
+        m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
+        'remove movie release year label
+        m.infoGroup.removeChild(m.top.findNode("releaseYear"))
+    end if
+
+    setFavoriteColor()
+    setWatchedColor()
+    SetUpVideoOptions(itemData.mediaSources)
+    SetUpAudioOptions(itemData.mediaStreams)
+    m.buttonGrp.visible = true
+    stopLoadingSpinner()
+end sub
+
+
+sub SetUpVideoOptions(streams)
+
+    videos = []
+    codecDetailsSet = false
+
+    for i = 0 to streams.Count() - 1
+        if streams[i].VideoType = "VideoFile"
+            codec = ""
+            if streams[i].mediaStreams <> invalid and streams[i].mediaStreams.Count() > 0
+
+                ' find the first (default) video track to get the codec for the details screen
+                if codecDetailsSet = false
+                    for index = 0 to streams[i].mediaStreams.Count() - 1
+                        if streams[i].mediaStreams[index].Type = "Video"
+                            setFieldText("video_codec", tr("Video") + ": " + streams[i].mediaStreams[index].displayTitle)
+                            codecDetailsSet = true
+                            exit for
+                        end if
+                    end for
+                end if
+
+                codec = streams[i].mediaStreams[0].displayTitle
+            end if
+
+            ' Create options for user to switch between video tracks
+            videos.push({
+                "Title": streams[i].Name,
+                "Description": tr("Video"),
+                "Selected": m.top.selectedVideoStreamId = streams[i].id,
+                "StreamID": streams[i].id,
+                "video_codec": codec
+            })
+        end if
+    end for
+
+    if streams.count() > 1
+        m.top.findnode("video_codec_count").text = "+" + stri(streams.Count() - 1).trim()
+    end if
+
+    options = {}
+    options.videos = videos
+    m.options.options = options
+
+end sub
+
+
+sub SetUpAudioOptions(streams)
+    tracks = []
+
+    for i = 0 to streams.Count() - 1
+        if streams[i].Type = "Audio"
+            tracks.push({ "Title": streams[i].displayTitle, "Description": streams[i].Title, "Selected": m.top.selectedAudioStreamIndex = i, "StreamIndex": i })
+        end if
+    end for
+
+    if tracks.count() > 1
+        m.top.findnode("audio_codec_count").text = "+" + stri(tracks.Count() - 1).trim()
+    end if
+
+    options = {}
+    if m.options.options.videos <> invalid
+        options.videos = m.options.options.videos
+    end if
+    options.audios = tracks
+    m.options.options = options
+
+end sub
+
+
+sub SetDefaultAudioTrack(itemData)
+    for i = 0 to itemData.mediaStreams.Count() - 1
+        if itemData.mediaStreams[i].Type = "Audio"
+            m.top.selectedAudioStreamIndex = i
+            setFieldText("audio_codec", tr("Audio") + ": " + itemData.mediaStreams[i].displayTitle)
+            exit for
+        end if
+    end for
+end sub
+
+sub setFieldText(field, value)
+    node = m.top.findNode(field)
+    if node = invalid or value = invalid then return
+
+    ' Handle non strings... Which _shouldn't_ happen, but hey
+    if type(value) = "roInt" or type(value) = "Integer"
+        value = str(value)
+    else if type(value) = "roFloat" or type(value) = "Float"
+        value = str(value)
+    else if type(value) <> "roString" and type(value) <> "String"
+        value = ""
+    end if
+
+    node.text = value
+end sub
+
+function getRuntime() as integer
+
+    itemData = m.top.itemContent.json
+
+    ' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
+    ' then 1/60 for seconds to minutess... 1/600,000,000
+    return round(itemData.RunTimeTicks / 600000000.0)
+end function
+
+function getEndTime() as string
+    itemData = m.top.itemContent.json
+
+    date = CreateObject("roDateTime")
+    duration_s = int(itemData.RunTimeTicks / 10000000.0)
+    date.fromSeconds(date.asSeconds() + duration_s)
+    date.toLocalTime()
+
+    return formatTime(date)
+end function
+
+sub setFavoriteColor()
+    fave = m.top.itemContent.favorite
+    fave_button = m.top.findNode("favorite-button")
+    if fave <> invalid and fave
+        fave_button.textColor = "#00ff00ff"
+        fave_button.focusedTextColor = "#269926ff"
+        fave_button.text = tr("Favorite")
+    else
+        fave_button.textColor = "0xddddddff"
+        fave_button.focusedTextColor = "#262626ff"
+        fave_button.text = tr("Set Favorite")
+    end if
+end sub
+
+sub setWatchedColor()
+    watched = m.top.itemContent.watched
+    watched_button = m.top.findNode("watched-button")
+    if watched
+        watched_button.textColor = "#ff0000ff"
+        watched_button.focusedTextColor = "#992626ff"
+        watched_button.text = tr("Watched")
+    else
+        watched_button.textColor = "0xddddddff"
+        watched_button.focusedTextColor = "#262626ff"
+        watched_button.text = tr("Set Watched")
+    end if
+end sub
+
+function round(f as float) as integer
+    ' BrightScript only has a "floor" round
+    ' This compares floor to floor + 1 to find which is closer
+    m = int(f)
+    n = m + 1
+    x = abs(f - m)
+    y = abs(f - n)
+    if y > x
+        return m
+    else
+        return n
+    end if
+end function
+
+'
+'Check if options updated and any reloading required
+sub audioOptionsClosed()
+    if m.options.audioStreamIndex <> m.top.selectedAudioStreamIndex
+        m.top.selectedAudioStreamIndex = m.options.audioStreamIndex
+        setFieldText("audio_codec", tr("Audio") + ": " + m.top.itemContent.json.mediaStreams[m.top.selectedAudioStreamIndex].displayTitle)
+    end if
+    m.top.findNode("buttons").setFocus(true)
+end sub
+
+'
+' Check if options were updated and if any reloding is needed...
+sub videoOptionsClosed()
+    if m.options.videoStreamId <> m.top.selectedVideoStreamId
+        m.top.selectedVideoStreamId = m.options.videoStreamId
+        setFieldText("video_codec", tr("Video") + ": " + m.options.video_codec)
+        ' Because the video stream has changed (i.e. the actual video)... we need to reload the audio stream choices for that video
+        m.top.unobservefield("itemContent")
+        itemData = m.top.itemContent.json
+        for each mediaSource in itemData.mediaSources
+            if mediaSource.id = m.top.selectedVideoStreamId
+                itemData.mediaStreams = []
+                for i = 0 to mediaSource.mediaStreams.Count() - 1
+                    itemData.mediaStreams.push(mediaSource.mediaStreams[i])
+                end for
+                SetDefaultAudioTrack(itemData)
+                SetUpAudioOptions(itemData.mediaStreams)
+                exit for
+            end if
+        end for
+        m.top.itemContent.json = itemData
+        m.top.observeField("itemContent", "itemContentChanged")
+    end if
+    m.top.findNode("buttons").setFocus(true)
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    ' Due to the way the button pressed event works, need to catch the release for the button as the press is being sent
+    ' directly to the main loop.  Will get this sorted in the layout update for Movie Details
+    if key = "OK" and m.top.findNode("options-button").isInFocusChain()
+        m.options.visible = true
+        m.options.setFocus(true)
+    end if
+
+    if key = "down" and m.buttonGrp.isInFocusChain()
+        m.top.lastFocus = m.extrasGrid
+        m.extrasGrid.setFocus(true)
+        m.top.findNode("VertSlider").reverse = false
+        m.top.findNode("extrasFader").reverse = false
+        m.top.findNode("pplAnime").control = "start"
+        return true
+    end if
+
+    if key = "up" and m.top.findNode("extrasGrid").isInFocusChain()
+        if m.extrasGrid.itemFocused = 0
+            m.top.lastFocus = m.buttonGrp
+            m.top.findNode("VertSlider").reverse = true
+            m.top.findNode("extrasFader").reverse = true
+            m.top.findNode("pplAnime").control = "start"
+            m.buttonGrp.setFocus(true)
+            return true
+        end if
+    end if
+
+    if not press then return false
+
+    if key = "back"
+        if m.options.visible = true
+            m.options.visible = false
+            videoOptionsClosed()
+            audioOptionsClosed()
+            return true
+        end if
+    else if key = "play" and m.extrasGrid.hasFocus()
+        print "Play was pressed from the movie details extras slider"
+        if m.extrasGrid.focusedItem <> invalid
+            m.top.quickPlayNode = m.extrasGrid.focusedItem
+            return true
+        end if
+    end if
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_movies_MovieOptions.bs.html b/docs/api/components_movies_MovieOptions.bs.html new file mode 100644 index 000000000..dd4d1e09e --- /dev/null +++ b/docs/api/components_movies_MovieOptions.bs.html @@ -0,0 +1,158 @@ +Source: components/movies/MovieOptions.bs
On this page

components_movies_MovieOptions.bs

sub init()
+
+    m.buttons = m.top.findNode("buttons")
+    m.buttons.buttons = [tr("Video"), tr("Audio")]
+    m.buttons.selectedIndex = 0
+    m.buttons.setFocus(true)
+
+    m.selectedItem = 0
+    m.selectedAudioIndex = 0
+    m.selectedVideoIndex = 0
+
+    m.menus = [m.top.findNode("videoMenu"), m.top.findNode("audioMenu")]
+
+    m.videoNames = []
+    m.audioNames = []
+
+    ' Set button colors to global
+    m.top.findNode("videoMenu").focusBitmapBlendColor = m.global.constants.colors.button
+    m.top.findNode("audioMenu").focusBitmapBlendColor = m.global.constants.colors.button
+
+    ' Animation
+    m.fadeAnim = m.top.findNode("fadeAnim")
+    m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
+    m.fadeInAnimOpacity = m.top.findNode("inOpacity")
+
+    m.buttons.observeField("focusedIndex", "buttonFocusChanged")
+    m.buttons.focusedIndex = m.selectedItem
+
+end sub
+
+sub optionsSet()
+    '  Videos Tab
+    if m.top.options.videos <> invalid
+        viewContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        selectedViewIndex = 0
+
+        for each view in m.top.options.videos
+            entry = viewContent.CreateChild("VideoTrackListData")
+            entry.title = view.Title
+            entry.description = view.Description
+            entry.streamId = view.streamId
+            entry.video_codec = view.video_codec
+            m.videoNames.push(view.Name)
+            if view.Selected <> invalid and view.Selected = true
+                selectedViewIndex = index
+                entry.selected = true
+                m.top.videoStreamId = view.streamId
+            end if
+            index = index + 1
+        end for
+
+        m.menus[0].content = viewContent
+        m.menus[0].jumpToItem = selectedViewIndex
+        m.menus[0].checkedItem = selectedViewIndex
+        m.selectedVideoIndex = selectedViewIndex
+    end if
+
+    '  audio Tab
+    if m.top.Options.audios <> invalid
+        audioContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        selectedAudioIndex = 0
+
+        for each audio in m.top.options.audios
+            entry = audioContent.CreateChild("AudioTrackListData")
+            entry.title = audio.Title
+            entry.description = audio.Description
+            entry.streamIndex = audio.StreamIndex
+            m.audioNames.push(audio.Name)
+            if audio.Selected <> invalid and audio.Selected = true
+                selectedAudioIndex = index
+                entry.selected = true
+                m.top.audioStreamIndex = audio.streamIndex
+            end if
+            index = index + 1
+        end for
+
+        m.menus[1].content = audioContent
+        m.menus[1].jumpToItem = selectedAudioIndex
+        m.menus[1].checkedItem = selectedAudioIndex
+        m.selectedAudioIndex = selectedAudioIndex
+    end if
+
+end sub
+
+' Switch menu shown when button focus changes
+sub buttonFocusChanged()
+    if m.buttons.focusedIndex = m.selectedItem then return
+    m.fadeOutAnimOpacity.fieldToInterp = m.menus[m.selectedItem].id + ".opacity"
+    m.fadeInAnimOpacity.fieldToInterp = m.menus[m.buttons.focusedIndex].id + ".opacity"
+    m.fadeAnim.control = "start"
+    m.selectedItem = m.buttons.focusedIndex
+end sub
+
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if key = "down" or (key = "OK" and m.top.findNode("buttons").hasFocus())
+        m.top.findNode("buttons").setFocus(false)
+        m.menus[m.selectedItem].setFocus(true)
+        m.menus[m.selectedItem].drawFocusFeedback = true
+
+        'If user presses down from button menu, focus first item.  If OK, focus checked item
+        if key = "down"
+            m.menus[m.selectedItem].jumpToItem = 0
+        else
+            m.menus[m.selectedItem].jumpToItem = m.menus[m.selectedItem].itemSelected
+        end if
+
+        return true
+    else if key = "OK"
+        if m.menus[m.selectedItem].isInFocusChain()
+            selMenu = m.menus[m.selectedItem]
+            selIndex = selMenu.itemSelected
+
+            'Handle Videos menu
+            if m.selectedItem = 0
+                if m.selectedVideoIndex = selIndex
+                else
+                    selMenu.content.GetChild(m.selectedVideoIndex).selected = false
+                    newSelection = selMenu.content.GetChild(selIndex)
+                    newSelection.selected = true
+                    m.selectedVideoIndex = selIndex
+                    m.top.videoStreamId = newSelection.streamId
+                    m.top.video_codec = newSelection.video_codec
+                end if
+                ' Then it is Audio options
+            else if m.selectedItem = 1
+                if m.selectedAudioIndex = selIndex
+                else
+                    selMenu.content.GetChild(m.selectedAudioIndex).selected = false
+                    newSelection = selMenu.content.GetChild(selIndex)
+                    newSelection.selected = true
+                    m.selectedAudioIndex = selIndex
+                    m.top.audioStreamIndex = newSelection.streamIndex
+                end if
+            end if
+
+        end if
+        return true
+    else if key = "back" or key = "up"
+        if m.menus[m.selectedItem].isInFocusChain()
+            m.buttons.setFocus(true)
+            m.menus[m.selectedItem].drawFocusFeedback = false
+            return true
+        end if
+    else if key = "options"
+        m.menus[m.selectedItem].drawFocusFeedback = false
+        return false
+    end if
+
+    return false
+
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_movies_VideoTrackListItem.bs.html b/docs/api/components_movies_VideoTrackListItem.bs.html new file mode 100644 index 000000000..b7ba017dd --- /dev/null +++ b/docs/api/components_movies_VideoTrackListItem.bs.html @@ -0,0 +1,36 @@ +Source: components/movies/VideoTrackListItem.bs
On this page

components_movies_VideoTrackListItem.bs

sub init()
+    m.title = m.top.findNode("title")
+    m.description = m.top.findNode("description")
+    m.selectedIcon = m.top.findNode("selectedIcon")
+end sub
+
+sub itemContentChanged()
+    m.title.text = m.top.itemContent.title
+    m.description.text = m.top.itemContent.description
+
+    if m.top.itemContent.description = ""
+        m.title.translation = [50, 20]
+    end if
+
+    if m.top.itemContent.selected
+        m.selectedIcon.uri = m.global.constants.icons.check_white
+    else
+        m.selectedIcon.uri = ""
+    end if
+
+end sub
+
+'
+'Scroll description if focused
+sub focusChanged()
+
+    if m.top.itemHasFocus = true
+        m.description.repeatCount = -1
+    else
+        m.description.repeatCount = 0
+    end if
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_AlbumGrid.bs.html b/docs/api/components_music_AlbumGrid.bs.html new file mode 100644 index 000000000..34ae2019b --- /dev/null +++ b/docs/api/components_music_AlbumGrid.bs.html @@ -0,0 +1,72 @@ +Source: components/music/AlbumGrid.bs
On this page

components_music_AlbumGrid.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    getData()
+end sub
+
+function getData()
+
+    ' If we have no album data, return a blank node
+    if m.top.MusicArtistAlbumData = invalid
+        data = CreateObject("roSGNode", "ContentNode")
+        return data
+    end if
+
+    albumData = m.top.MusicArtistAlbumData
+    data = CreateObject("roSGNode", "ContentNode")
+
+    for each album in albumData.items
+        gridAlbum = CreateObject("roSGNode", "ContentNode")
+
+        if not isValid(album.posterURL) or album.posterURL = ""
+            album.posterURL = "pkg:/images/icons/album.png"
+        end if
+
+        gridAlbum.shortdescriptionline1 = album.title
+        gridAlbum.HDGRIDPOSTERURL = album.posterURL
+        gridAlbum.hdposterurl = album.posterURL
+        gridAlbum.SDGRIDPOSTERURL = album.posterURL
+        gridAlbum.sdposterurl = album.posterURL
+
+        data.appendChild(gridAlbum)
+    end for
+
+    m.top.content = data
+
+    return data
+end function
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "up"
+        if m.top.itemFocused <= 4
+            m.top.escape = key
+            return true
+        end if
+    else if key = "left"
+        if m.top.itemFocused mod 5 = 0
+            m.top.escape = key
+            return true
+        end if
+    else if key = "right"
+        if m.top.itemFocused + 1 mod 5 = 0
+            m.top.escape = key
+            return true
+        end if
+    else if key = "down"
+        totalCount = m.top.MusicArtistAlbumData.items.count()
+        totalRows = div_ceiling(totalCount, 5)
+        currentRow = div_ceiling(m.top.itemFocused + 1, 5)
+
+        if currentRow = totalRows
+            m.top.escape = key
+            return true
+        end if
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_AlbumTrackList.bs.html b/docs/api/components_music_AlbumTrackList.bs.html new file mode 100644 index 000000000..1ddcd3345 --- /dev/null +++ b/docs/api/components_music_AlbumTrackList.bs.html @@ -0,0 +1,28 @@ +Source: components/music/AlbumTrackList.bs
On this page

components_music_AlbumTrackList.bs

sub init()
+    m.top.content = getData()
+    m.top.setfocus(true)
+end sub
+
+function getData()
+    if m.top.MusicArtistAlbumData = invalid
+        data = CreateObject("roSGNode", "ContentNode")
+        return data
+    end if
+
+    albumData = m.top.MusicArtistAlbumData
+    data = CreateObject("roSGNode", "ContentNode")
+
+    for each song in albumData.items
+        songcontent = data.createChild("MusicSongData")
+        songcontent.json = song.json
+    end for
+
+    m.top.content = data
+
+    m.top.doneLoading = true
+
+    return data
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_AlbumView.bs.html b/docs/api/components_music_AlbumView.bs.html new file mode 100644 index 000000000..fe87ff39b --- /dev/null +++ b/docs/api/components_music_AlbumView.bs.html @@ -0,0 +1,172 @@ +Source: components/music/AlbumView.bs
On this page

components_music_AlbumView.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+    setupMainNode()
+
+    m.playAlbum = m.top.findNode("playAlbum")
+    m.instantMix = m.top.findNode("instantMix")
+    m.albumCover = m.top.findNode("albumCover")
+    m.songList = m.top.findNode("songList")
+    m.infoGroup = m.top.FindNode("infoGroup")
+    m.songListRect = m.top.FindNode("songListRect")
+
+    m.songList.observeField("doneLoading", "onDoneLoading")
+
+    m.dscr = m.top.findNode("overview")
+    createDialogPallete()
+end sub
+
+sub setupMainNode()
+    main = m.top.findNode("toplevel")
+    main.translation = [96, 175]
+end sub
+
+' Set values for displayed values on screen
+sub pageContentChanged()
+    item = m.top.pageContent
+
+    setPosterImage(item.posterURL)
+    setScreenTitle(item.json)
+    setOnScreenTextValues(item.json)
+
+    ' Only 1 song shown, so hide Play Album button
+    if item.json.ChildCount = 1
+        m.playAlbum.visible = false
+    end if
+end sub
+
+' Set poster image on screen
+sub setPosterImage(posterURL)
+    if isValid(posterURL)
+        m.albumCover.uri = posterURL
+    end if
+end sub
+
+' Set screen's title text
+sub setScreenTitle(json)
+    newTitle = ""
+    if isValid(json)
+        if isValid(json.AlbumArtist)
+            newTitle = json.AlbumArtist
+        end if
+        if isValid(json.AlbumArtist) and isValid(json.name)
+            newTitle = newTitle + " / "
+        end if
+        if isValid(json.name)
+            newTitle = newTitle + json.name
+        end if
+    end if
+    m.top.overhangTitle = newTitle
+end sub
+
+' Adjust scene by removing overview node and showing more songs
+sub adjustScreenForNoOverview()
+    m.infoGroup.removeChild(m.dscr)
+    m.songListRect.height = 800
+    m.songList.numRows = 12
+end sub
+
+' Populate on screen text variables
+sub setOnScreenTextValues(json)
+    if isValid(json)
+        if isValid(json.overview) and json.overview <> ""
+            ' We have overview text
+            setFieldTextValue("overview", json.overview)
+        else
+            ' We don't have overview text
+            adjustScreenForNoOverview()
+        end if
+
+        setFieldTextValue("numberofsongs", stri(json.ChildCount) + " Tracks")
+
+        if type(json.ProductionYear) = "roInt"
+            setFieldTextValue("released", "Released " + stri(json.ProductionYear))
+        end if
+
+        if json.genres.count() > 0
+            setFieldTextValue("genres", json.genres.join(", "))
+        end if
+
+        if type(json.RunTimeTicks) = "LongInteger"
+            setFieldTextValue("runtime", stri(getMinutes(json.RunTimeTicks)) + " mins")
+        end if
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "options"
+        if m.dscr.isTextEllipsized
+            createFullDscrDlg()
+            return true
+        end if
+        return false
+    end if
+
+    if key = "right"
+        if m.playAlbum.hasFocus() or m.instantMix.hasFocus()
+            m.songList.setFocus(true)
+            return true
+        end if
+    else if key = "left" and m.songList.hasFocus()
+        if m.playAlbum.visible
+            m.playAlbum.setFocus(true)
+        else if m.instantMix.visible
+            m.instantMix.setFocus(true)
+        else
+            return false
+        end if
+        return true
+    else if key = "down" and m.playAlbum.hasFocus()
+        m.instantMix.setFocus(true)
+        return true
+    else if key = "up" and m.instantMix.hasFocus()
+        m.playAlbum.setFocus(true)
+        return true
+    end if
+
+    return false
+end function
+
+sub createFullDscrDlg()
+    dlg = CreateObject("roSGNode", "OverviewDialog")
+    dlg.Title = tr("Press 'Back' to Close")
+    dlg.width = 1290
+    dlg.palette = m.dlgPalette
+    dlg.overview = [m.dscr.text]
+    m.fullDscrDlg = dlg
+    m.top.getScene().dialog = dlg
+    border = createObject("roSGNode", "Poster")
+    border.uri = "pkg:/images/hd_focul_9.png"
+    border.blendColor = "#c9c9c9ff"
+    border.width = dlg.width + 6
+    border.height = dlg.height + 6
+    border.translation = [dlg.translation[0] - 3, dlg.translation[1] - 3]
+    border.visible = true
+end sub
+
+sub createDialogPallete()
+    m.dlgPalette = createObject("roSGNode", "RSGPalette")
+    m.dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogItemColor: "0x00EF00FF",
+        DialogTextColor: "0xb0b0b0FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "0xcc7ecc4D",
+        DialogInputFieldColor: "0x80FF8080",
+        DialogKeyboardColor: "0x80FF804D",
+        DialogFootprintColor: "0x80FF804D"
+    }
+end sub
+
+sub onDoneLoading()
+    m.songList.unobservefield("doneLoading")
+    stopLoadingSpinner()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_ArtistView.bs.html b/docs/api/components_music_ArtistView.bs.html new file mode 100644 index 000000000..0123c2d13 --- /dev/null +++ b/docs/api/components_music_ArtistView.bs.html @@ -0,0 +1,337 @@ +Source: components/music/ArtistView.bs
On this page

components_music_ArtistView.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+    setupMainNode()
+    setupButtons()
+
+    m.remoteButtonsActive = true
+
+    m.albumHeader = m.top.findNode("albumHeader")
+    m.albumHeader.text = tr("Albums")
+
+    m.appearsOnHeader = m.top.findNode("appearsOnHeader")
+    m.appearsOnHeader.text = tr("AppearsOn")
+
+    m.appearsOn = m.top.findNode("appearsOn")
+    m.appearsOn.observeField("escape", "onAppearsOnEscape")
+    m.appearsOn.observeField("MusicArtistAlbumData", "onAppearsOnData")
+
+    m.albums = m.top.findNode("albums")
+    m.albums.observeField("escape", "onAlbumsEscape")
+    m.albums.observeField("MusicArtistAlbumData", "onAlbumsData")
+
+    m.pageLoadAnimation = m.top.findNode("pageLoad")
+    m.pageLoadAnimation.control = "start"
+
+    m.sectionNavigation = m.top.findNode("sectionNavigation")
+    m.sectionNavigation.observeField("escape", "onSectionNavigationEscape")
+    m.sectionNavigation.observeField("selected", "onSectionNavigationSelected")
+
+    m.sectionScroller = m.top.findNode("sectionScroller")
+    m.sectionScroller.observeField("displayedIndex", "onSectionScrollerChange")
+    m.overhang = m.top.getScene().findNode("overhang")
+
+    ' Load background image
+    m.LoadBackdropImageTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadBackdropImageTask.itemsToLoad = "backdropImage"
+
+    m.backDrop = m.top.findNode("backdrop")
+    m.artistImage = m.top.findNode("artistImage")
+    m.dscr = m.top.findNode("overview")
+    m.dscr.observeField("isTextEllipsized", "onEllipsisChanged")
+    createDialogPallete()
+end sub
+
+sub onAlbumsData()
+    ' We have no album data
+    if m.albums.MusicArtistAlbumData.TotalRecordCount = 0
+        m.sectionScroller.removeChild(m.top.findNode("albumsSlide"))
+        m.sectionNavigation.removeChild(m.top.findNode("albumsLink"))
+        m.top.findNode("appearsOnSlide").callFunc("scrollUpToOnDeck")
+    end if
+end sub
+
+sub onAppearsOnData()
+    ' We have no appears on data
+    if m.appearsOn.MusicArtistAlbumData.TotalRecordCount = 0
+        m.sectionScroller.removeChild(m.top.findNode("appearsOnSlide"))
+        m.sectionNavigation.removeChild(m.top.findNode("appearsOnLink"))
+    end if
+end sub
+
+sub onSectionScrollerChange()
+    m.overhang.isVisible = (m.sectionScroller.displayedIndex = 0)
+end sub
+
+sub OnScreenShown()
+    m.sectionScroller.focus = true
+
+    if m.sectionScroller.displayedIndex = 0
+        m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
+        m.top.selectedButtonIndex = 0
+        m.buttonGrp.setFocus(true)
+    else
+        m.overhang.opacity = "0"
+        m.overhang.isVisible = false
+        m.overhang.opacity = "1"
+    end if
+    stopLoadingSpinner()
+end sub
+
+sub OnScreenHidden()
+    if not m.overhang.isVisible
+        m.overhang.disableMoveAnimation = true
+        m.overhang.isVisible = true
+        m.overhang.disableMoveAnimation = false
+        m.overhang.opacity = "1"
+    end if
+end sub
+
+sub onAlbumsEscape()
+    if m.albums.escape = "up"
+        m.sectionNavigation.selected = m.sectionScroller.displayedIndex - 1
+    else if m.albums.escape = "left"
+        m.sectionNavigation.setFocus(true)
+    else if m.albums.escape = "down"
+        if m.sectionScroller.displayedIndex + 1 < m.sectionNavigation.getChildCount()
+            m.sectionNavigation.selected = m.sectionScroller.displayedIndex + 1
+        end if
+    end if
+end sub
+
+sub onAppearsOnEscape()
+    if m.appearsOn.escape = "up"
+        m.sectionNavigation.selected = m.sectionScroller.displayedIndex - 1
+    else if m.appearsOn.escape = "left"
+        m.sectionNavigation.setFocus(true)
+    end if
+end sub
+
+' Setup playback buttons, default to Play button selected
+sub setupButtons()
+    m.buttonGrp = m.top.findNode("buttons")
+    m.buttonCount = m.buttonGrp.getChildCount()
+
+    m.playButton = m.top.findNode("play")
+    m.previouslySelectedButtonIndex = -1
+
+    m.top.observeField("selectedButtonIndex", "onButtonSelectedChange")
+    m.top.selectedButtonIndex = 0
+end sub
+
+' Event handler when user selected a different playback button
+sub onButtonSelectedChange()
+    ' Change previously selected button back to default image
+    if m.previouslySelectedButtonIndex > -1
+        previousSelectedButton = m.buttonGrp.getChild(m.previouslySelectedButtonIndex)
+        previousSelectedButton.focus = false
+    end if
+
+    ' Change selected button image to selected image
+    selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex)
+    selectedButton.focus = true
+end sub
+
+sub setupMainNode()
+    m.main = m.top.findNode("toplevel")
+    m.main.translation = [120, 175]
+end sub
+
+' Event fired when page data is loaded
+sub pageContentChanged()
+    item = m.top.pageContent
+
+    ' Use metadata to load backdrop image
+    m.LoadBackdropImageTask.itemId = item.json.id
+    m.LoadBackdropImageTask.observeField("content", "onBackdropImageLoaded")
+    m.LoadBackdropImageTask.control = "RUN"
+
+    ' Populate scene data
+    setScreenTitle(item.json)
+    setPosterImage(item.posterURL)
+end sub
+
+sub setScreenTitle(json)
+    if isValid(json)
+        m.top.overhangTitle = json.name
+    end if
+end sub
+
+sub setPosterImage(posterURL)
+    if not isValid(posterURL) or posterURL = ""
+        posterURL = "pkg:/images/missingArtist.png"
+    end if
+
+    m.artistImage.uri = posterURL
+end sub
+
+sub onBackdropImageLoaded()
+    data = m.LoadBackdropImageTask.content[0]
+    m.LoadBackdropImageTask.unobserveField("content")
+    if isValid(data) and data <> ""
+        setBackdropImage(data)
+    end if
+end sub
+
+' Add backdrop image to screen
+sub setBackdropImage(data)
+    if isValid(data)
+        if m.backDrop.uri <> data
+            m.backDrop.uri = data
+        end if
+    end if
+end sub
+
+' Event fired when page data is loaded
+sub artistOverviewChanged()
+    overviewContent = m.top.artistOverview
+
+    if isValid(overviewContent)
+        setFieldTextValue("overview", overviewContent)
+    end if
+end sub
+
+sub onEllipsisChanged()
+    if m.dscr.isTextEllipsized
+        dscrShowFocus()
+    end if
+end sub
+
+sub onSectionNavigationEscape()
+    if m.sectionNavigation.escape = "right"
+        m.sectionNavigation.setFocus(false)
+        m.remoteButtonsActive = false
+        m.sectionScroller.focus = true
+    end if
+end sub
+
+sub onSectionNavigationSelected()
+    m.sectionScroller.displayedIndex = m.sectionNavigation.selected
+end sub
+
+sub dscrShowFocus()
+    if m.dscr.isTextEllipsized
+        m.dscr.setFocus(true)
+        m.dscr.opacity = 1.0
+    end if
+end sub
+
+sub createFullDscrDlg()
+    dlg = CreateObject("roSGNode", "OverviewDialog")
+    dlg.Title = tr("Press 'Back' to Close")
+    dlg.width = 1290
+    dlg.palette = m.dlgPalette
+    dlg.overview = [m.dscr.text]
+    m.fullDscrDlg = dlg
+    m.top.getScene().dialog = dlg
+    border = createObject("roSGNode", "Poster")
+    border.uri = "pkg:/images/hd_focul_9.png"
+    border.blendColor = "#c9c9c9ff"
+    border.width = dlg.width + 6
+    border.height = dlg.height + 6
+    border.translation = [dlg.translation[0] - 3, dlg.translation[1] - 3]
+    border.visible = true
+end sub
+
+sub createDialogPallete()
+    m.dlgPalette = createObject("roSGNode", "RSGPalette")
+    m.dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogItemColor: "0x00EF00FF",
+        DialogTextColor: "0xb0b0b0FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "0xcc7ecc4D",
+        DialogInputFieldColor: "0x80FF8080",
+        DialogKeyboardColor: "0x80FF804D",
+        DialogFootprintColor: "0x80FF804D"
+    }
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if m.buttonGrp.isInFocusChain()
+        if key = "OK"
+            if press
+                selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex)
+                selectedButton.selected = not selectedButton.selected
+                return true
+            end if
+        end if
+
+        if key = "left"
+            if m.top.selectedButtonIndex > 0
+                m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
+                m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1
+                return true
+            end if
+
+            if press
+                selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex)
+                selectedButton.focus = false
+
+                m.sectionNavigation.setFocus(true)
+                return true
+            end if
+
+            return false
+        end if
+
+        if key = "right"
+            if m.top.pageContent.count() = 1 then return false
+
+            if m.buttonGrp.getChild(m.top.selectedButtonIndex).escape = "right"
+                m.buttonGrp.getChild(m.top.selectedButtonIndex).escape = ""
+                m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
+
+                if m.top.selectedButtonIndex < m.buttonCount - 1
+                    m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
+                end if
+
+                return true
+            end if
+        end if
+
+        if key = "down"
+            if m.sectionNavigation.getChildCount() > 1
+                selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex)
+                selectedButton.focus = false
+
+                m.top.selectedButtonIndex = 0
+                m.sectionNavigation.selected = m.sectionScroller.displayedIndex + 1
+            end if
+        end if
+    end if
+
+    if not press then return false
+
+    if key = "options"
+        if m.dscr.isTextEllipsized
+            createFullDscrDlg()
+            return true
+        end if
+    end if
+
+    if key = "play"
+        print "play button pressed from ArtistView"
+        itemToPlay = invalid
+
+        if isValid(m.albums) and m.albums.isInFocusChain()
+            itemToPlay = m.albums.MusicArtistAlbumData.items[m.albums.itemFocused]
+        else if isValid(m.appearsOn) and m.appearsOn.isInFocusChain()
+            itemToPlay = m.appearsOn.MusicArtistAlbumData.items[m.appearsOn.itemFocused]
+        end if
+
+        if isValid(itemToPlay)
+            m.top.quickPlayNode = itemToPlay
+            return true
+        end if
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_AudioPlayerView.bs.html b/docs/api/components_music_AudioPlayerView.bs.html new file mode 100644 index 000000000..875805348 --- /dev/null +++ b/docs/api/components_music_AudioPlayerView.bs.html @@ -0,0 +1,602 @@ +Source: components/music/AudioPlayerView.bs
On this page

components_music_AudioPlayerView.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+
+    setupAudioNode()
+    setupAnimationTasks()
+    setupButtons()
+    setupInfoNodes()
+    setupDataTasks()
+    setupScreenSaver()
+
+    m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
+
+    m.buttonCount = m.buttons.getChildCount()
+
+    m.screenSaverTimeout = 300
+
+    m.LoadScreenSaverTimeoutTask.observeField("content", "onScreensaverTimeoutLoaded")
+    m.LoadScreenSaverTimeoutTask.control = "RUN"
+
+    m.di = CreateObject("roDeviceInfo")
+
+    ' Write screen tracker for screensaver
+    WriteAsciiFile("tmp:/scene.temp", "nowplaying")
+    MoveFile("tmp:/scene.temp", "tmp:/scene")
+
+    loadButtons()
+    pageContentChanged()
+    setShuffleIconState()
+    setLoopButtonImage()
+end sub
+
+sub onScreensaverTimeoutLoaded()
+    data = m.LoadScreenSaverTimeoutTask.content
+    m.LoadScreenSaverTimeoutTask.unobserveField("content")
+    if isValid(data)
+        m.screenSaverTimeout = data
+    end if
+end sub
+
+sub setupScreenSaver()
+    m.screenSaverBackground = m.top.FindNode("screenSaverBackground")
+
+    ' Album Art Screensaver
+    m.screenSaverAlbumCover = m.top.FindNode("screenSaverAlbumCover")
+    m.screenSaverAlbumAnimation = m.top.findNode("screenSaverAlbumAnimation")
+    m.screenSaverAlbumCoverFadeIn = m.top.findNode("screenSaverAlbumCoverFadeIn")
+
+    ' Jellyfin Screensaver
+    m.PosterOne = m.top.findNode("PosterOne")
+    m.PosterOne.uri = "pkg:/images/logo.png"
+    m.BounceAnimation = m.top.findNode("BounceAnimation")
+    m.PosterOneFadeIn = m.top.findNode("PosterOneFadeIn")
+end sub
+
+sub setupAnimationTasks()
+    m.displayButtonsAnimation = m.top.FindNode("displayButtonsAnimation")
+    m.playPositionAnimation = m.top.FindNode("playPositionAnimation")
+    m.playPositionAnimationWidth = m.top.FindNode("playPositionAnimationWidth")
+
+    m.bufferPositionAnimation = m.top.FindNode("bufferPositionAnimation")
+    m.bufferPositionAnimationWidth = m.top.FindNode("bufferPositionAnimationWidth")
+
+    m.screenSaverStartAnimation = m.top.FindNode("screenSaverStartAnimation")
+end sub
+
+' Creates tasks to gather data needed to render Scene and play song
+sub setupDataTasks()
+    ' Load meta data
+    m.LoadMetaDataTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadMetaDataTask.itemsToLoad = "metaData"
+
+    ' Load background image
+    m.LoadBackdropImageTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadBackdropImageTask.itemsToLoad = "backdropImage"
+
+    ' Load audio stream
+    m.LoadAudioStreamTask = CreateObject("roSGNode", "LoadItemsTask")
+    m.LoadAudioStreamTask.itemsToLoad = "audioStream"
+
+    m.LoadScreenSaverTimeoutTask = CreateObject("roSGNode", "LoadScreenSaverTimeoutTask")
+end sub
+
+' Creates audio node used to play song(s)
+sub setupAudioNode()
+    m.global.audioPlayer.observeField("state", "audioStateChanged")
+    m.global.audioPlayer.observeField("position", "audioPositionChanged")
+    m.global.audioPlayer.observeField("bufferingStatus", "bufferPositionChanged")
+end sub
+
+' Setup playback buttons, default to Play button selected
+sub setupButtons()
+    m.buttons = m.top.findNode("buttons")
+    m.top.observeField("selectedButtonIndex", "onButtonSelectedChange")
+    m.previouslySelectedButtonIndex = 1
+    m.top.selectedButtonIndex = 2
+end sub
+
+' Event handler when user selected a different playback button
+sub onButtonSelectedChange()
+    ' Change previously selected button back to default image
+    selectedButton = m.buttons.getChild(m.previouslySelectedButtonIndex)
+    selectedButton.uri = selectedButton.uri.Replace("-selected", "-default")
+
+    ' Change selected button image to selected image
+    selectedButton = m.buttons.getChild(m.top.selectedButtonIndex)
+    selectedButton.uri = selectedButton.uri.Replace("-default", "-selected")
+end sub
+
+sub setupInfoNodes()
+    m.albumCover = m.top.findNode("albumCover")
+    m.backDrop = m.top.findNode("backdrop")
+    m.playPosition = m.top.findNode("playPosition")
+    m.bufferPosition = m.top.findNode("bufferPosition")
+    m.seekBar = m.top.findNode("seekBar")
+    m.shuffleIndicator = m.top.findNode("shuffleIndicator")
+    m.loopIndicator = m.top.findNode("loopIndicator")
+    m.positionTimestamp = m.top.findNode("positionTimestamp")
+    m.totalLengthTimestamp = m.top.findNode("totalLengthTimestamp")
+end sub
+
+sub bufferPositionChanged()
+    if not isValid(m.global.audioPlayer.bufferingStatus)
+        bufferPositionBarWidth = m.seekBar.width
+    else
+        bufferPositionBarWidth = m.seekBar.width * m.global.audioPlayer.bufferingStatus.percentage
+    end if
+
+    ' Ensure position bar is never wider than the seek bar
+    if bufferPositionBarWidth > m.seekBar.width
+        bufferPositionBarWidth = m.seekBar.width
+    end if
+
+    ' Use animation to make the display smooth
+    m.bufferPositionAnimationWidth.keyValue = [m.bufferPosition.width, bufferPositionBarWidth]
+    m.bufferPositionAnimation.control = "start"
+end sub
+
+sub audioPositionChanged()
+    if m.global.audioPlayer.position = 0
+        m.playPosition.width = 0
+    end if
+
+    if not isValid(m.global.audioPlayer.position)
+        playPositionBarWidth = 0
+    else if not isValid(m.songDuration)
+        playPositionBarWidth = 0
+    else
+        songPercentComplete = m.global.audioPlayer.position / m.songDuration
+        playPositionBarWidth = m.seekBar.width * songPercentComplete
+    end if
+
+    ' Ensure position bar is never wider than the seek bar
+    if playPositionBarWidth > m.seekBar.width
+        playPositionBarWidth = m.seekBar.width
+    end if
+
+    ' Use animation to make the display smooth
+    m.playPositionAnimationWidth.keyValue = [m.playPosition.width, playPositionBarWidth]
+    m.playPositionAnimation.control = "start"
+
+    ' Update displayed position timestamp
+    if isValid(m.global.audioPlayer.position)
+        m.positionTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false)
+    else
+        m.positionTimestamp.text = "0:00"
+    end if
+
+    ' Only fall into screensaver logic if the user has screensaver enabled in Roku settings
+    if m.screenSaverTimeout > 0
+        if m.di.TimeSinceLastKeypress() >= m.screenSaverTimeout - 2
+            if not screenSaverActive()
+                startScreenSaver()
+            end if
+        end if
+    end if
+end sub
+
+function screenSaverActive() as boolean
+    return m.screenSaverBackground.visible or m.screenSaverAlbumCover.opacity > 0 or m.PosterOne.opacity > 0
+end function
+
+sub startScreenSaver()
+    m.screenSaverBackground.visible = true
+    m.top.overhangVisible = false
+
+    if m.albumCover.uri = ""
+        ' Jellyfin Logo Screensaver
+        m.PosterOne.visible = true
+        m.PosterOneFadeIn.control = "start"
+        m.BounceAnimation.control = "start"
+    else
+        ' Album Art Screensaver
+        m.screenSaverAlbumCoverFadeIn.control = "start"
+        m.screenSaverAlbumAnimation.control = "start"
+    end if
+end sub
+
+sub endScreenSaver()
+    m.PosterOneFadeIn.control = "pause"
+    m.screenSaverAlbumCoverFadeIn.control = "pause"
+    m.screenSaverAlbumAnimation.control = "pause"
+    m.BounceAnimation.control = "pause"
+    m.screenSaverBackground.visible = false
+    m.screenSaverAlbumCover.opacity = 0
+    m.PosterOne.opacity = 0
+    m.top.overhangVisible = true
+end sub
+
+sub audioStateChanged()
+
+    ' Song Finished, attempt to move to next song
+    if m.global.audioPlayer.state = "finished"
+        ' User has enabled single song loop, play current song again
+        if m.global.audioPlayer.loopMode = "one"
+            playAction()
+            return
+        end if
+
+        if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
+            m.top.state = "finished"
+        else
+            ' We are at the end of the song queue
+
+            ' User has enabled loop for entire song queue, move back to first song
+            if m.global.audioPlayer.loopMode = "all"
+                m.global.queueManager.callFunc("setPosition", -1)
+                LoadNextSong()
+                return
+            end if
+
+            ' Return to previous screen
+            m.top.state = "finished"
+        end if
+    end if
+end sub
+
+function playAction() as boolean
+    if m.global.audioPlayer.state = "playing"
+        m.global.audioPlayer.control = "pause"
+        ' Allow screen to go to real screensaver
+        WriteAsciiFile("tmp:/scene.temp", "nowplaying-paused")
+        MoveFile("tmp:/scene.temp", "tmp:/scene")
+    else if m.global.audioPlayer.state = "paused"
+        m.global.audioPlayer.control = "resume"
+        ' Write screen tracker for screensaver
+        WriteAsciiFile("tmp:/scene.temp", "nowplaying")
+        MoveFile("tmp:/scene.temp", "tmp:/scene")
+    else if m.global.audioPlayer.state = "finished"
+        m.global.audioPlayer.control = "play"
+        ' Write screen tracker for screensaver
+        WriteAsciiFile("tmp:/scene.temp", "nowplaying")
+        MoveFile("tmp:/scene.temp", "tmp:/scene")
+    end if
+
+    return true
+end function
+
+function previousClicked() as boolean
+    if m.playlistTypeCount > 1 then return false
+    if m.global.queueManager.callFunc("getPosition") = 0 then return false
+
+    if m.global.audioPlayer.state = "playing"
+        m.global.audioPlayer.control = "stop"
+    end if
+
+    ' Reset loop mode due to manual user interaction
+    if m.global.audioPlayer.loopMode = "one"
+        resetLoopModeToDefault()
+    end if
+
+    m.global.queueManager.callFunc("moveBack")
+    pageContentChanged()
+
+
+    return true
+end function
+
+sub resetLoopModeToDefault()
+    m.global.audioPlayer.loopMode = ""
+    setLoopButtonImage()
+end sub
+
+function loopClicked() as boolean
+
+    if m.global.audioPlayer.loopMode = ""
+        m.global.audioPlayer.loopMode = "all"
+    else if m.global.audioPlayer.loopMode = "all"
+        m.global.audioPlayer.loopMode = "one"
+    else
+        m.global.audioPlayer.loopMode = ""
+    end if
+
+    setLoopButtonImage()
+
+    return true
+end function
+
+sub setLoopButtonImage()
+    if m.global.audioPlayer.loopMode = "all"
+        m.loopIndicator.opacity = "1"
+        m.loopIndicator.uri = m.loopIndicator.uri.Replace("-off", "-on")
+    else if m.global.audioPlayer.loopMode = "one"
+        m.loopIndicator.uri = m.loopIndicator.uri.Replace("-on", "1-on")
+    else
+        m.loopIndicator.uri = m.loopIndicator.uri.Replace("1-on", "-off")
+    end if
+end sub
+
+function nextClicked() as boolean
+    if m.playlistTypeCount > 1 then return false
+
+    ' Reset loop mode due to manual user interaction
+    if m.global.audioPlayer.loopMode = "one"
+        resetLoopModeToDefault()
+    end if
+
+    if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
+        LoadNextSong()
+    end if
+
+    return true
+end function
+
+sub toggleShuffleEnabled()
+    m.global.queueManager.callFunc("toggleShuffle")
+end sub
+
+function findCurrentSongIndex(songList) as integer
+    if not isValidAndNotEmpty(songList) then return 0
+
+    for i = 0 to songList.count() - 1
+        if songList[i].id = m.global.queueManager.callFunc("getCurrentItem").id
+            return i
+        end if
+    end for
+
+    return 0
+end function
+
+function shuffleClicked() as boolean
+
+    currentSongIndex = findCurrentSongIndex(m.global.queueManager.callFunc("getUnshuffledQueue"))
+
+    toggleShuffleEnabled()
+
+    if not m.global.queueManager.callFunc("getIsShuffled")
+        m.shuffleIndicator.opacity = ".4"
+        m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-on", "-off")
+        m.global.queueManager.callFunc("setPosition", currentSongIndex)
+        setTrackNumberDisplay()
+        return true
+    end if
+
+    m.shuffleIndicator.opacity = "1"
+    m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
+    setTrackNumberDisplay()
+
+    return true
+end function
+
+sub setShuffleIconState()
+    if m.global.queueManager.callFunc("getIsShuffled")
+        m.shuffleIndicator.opacity = "1"
+        m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
+    end if
+end sub
+
+sub setTrackNumberDisplay()
+    setFieldTextValue("numberofsongs", "Track " + stri(m.global.queueManager.callFunc("getPosition") + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
+end sub
+
+sub LoadNextSong()
+    if m.global.audioPlayer.state = "playing"
+        m.global.audioPlayer.control = "stop"
+    end if
+
+    ' Reset playPosition bar without animation
+    m.playPosition.width = 0
+    m.global.queueManager.callFunc("moveForward")
+    pageContentChanged()
+end sub
+
+' Update values on screen when page content changes
+sub pageContentChanged()
+
+    ' Reset buffer bar without animation
+    m.bufferPosition.width = 0
+
+    useMetaTask = false
+    currentItem = m.global.queueManager.callFunc("getCurrentItem")
+
+    if not isValid(currentItem.RunTimeTicks)
+        useMetaTask = true
+    end if
+
+    if not isValid(currentItem.AlbumArtist)
+        useMetaTask = true
+    end if
+
+    if not isValid(currentItem.name)
+        useMetaTask = true
+    end if
+
+    if not isValid(currentItem.Artists)
+        useMetaTask = true
+    end if
+
+    if useMetaTask
+        m.LoadMetaDataTask.itemId = currentItem.id
+        m.LoadMetaDataTask.observeField("content", "onMetaDataLoaded")
+        m.LoadMetaDataTask.control = "RUN"
+    else
+        if isValid(currentItem.ParentBackdropItemId)
+            setBackdropImage(ImageURL(currentItem.ParentBackdropItemId, "Backdrop", { "maxHeight": "720", "maxWidth": "1280" }))
+        end if
+
+        setPosterImage(ImageURL(currentItem.id, "Primary", { "maxHeight": 500, "maxWidth": 500 }))
+        setScreenTitle(currentItem)
+        setOnScreenTextValues(currentItem)
+        m.songDuration = currentItem.RunTimeTicks / 10000000.0
+
+        ' Update displayed total audio length
+        m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks)
+    end if
+
+    m.LoadAudioStreamTask.itemId = currentItem.id
+    m.LoadAudioStreamTask.observeField("content", "onAudioStreamLoaded")
+    m.LoadAudioStreamTask.control = "RUN"
+end sub
+
+' If we have more and 1 song to play, fade in the next and previous controls
+sub loadButtons()
+    ' Don't show audio buttons if we have a mixed playlist
+    if m.playlistTypeCount > 1 then return
+
+    if m.global.queueManager.callFunc("getCount") > 1
+        m.shuffleIndicator.opacity = ".4"
+        m.loopIndicator.opacity = ".4"
+        m.displayButtonsAnimation.control = "start"
+        setLoopButtonImage()
+    end if
+end sub
+
+sub onAudioStreamLoaded()
+    stopLoadingSpinner()
+    data = m.LoadAudioStreamTask.content[0]
+    m.LoadAudioStreamTask.unobserveField("content")
+    if data <> invalid and data.count() > 0
+        m.global.audioPlayer.content = data
+        m.global.audioPlayer.control = "none"
+        m.global.audioPlayer.control = "play"
+    end if
+end sub
+
+sub onBackdropImageLoaded()
+    data = m.LoadBackdropImageTask.content[0]
+    m.LoadBackdropImageTask.unobserveField("content")
+    if isValid(data) and data <> ""
+        setBackdropImage(data)
+    end if
+end sub
+
+sub onMetaDataLoaded()
+    data = m.LoadMetaDataTask.content[0]
+    m.LoadMetaDataTask.unobserveField("content")
+    if isValid(data) and data.count() > 0 and isValid(data.json)
+        ' Use metadata to load backdrop image
+        if isValid(data.json.ArtistItems) and isValid(data.json.ArtistItems[0]) and isValid(data.json.ArtistItems[0].id)
+            m.LoadBackdropImageTask.itemId = data.json.ArtistItems[0].id
+            m.LoadBackdropImageTask.observeField("content", "onBackdropImageLoaded")
+            m.LoadBackdropImageTask.control = "RUN"
+        end if
+
+        setPosterImage(data.posterURL)
+        setScreenTitle(data.json)
+        setOnScreenTextValues(data.json)
+
+        if isValid(data.json.RunTimeTicks)
+            m.songDuration = data.json.RunTimeTicks / 10000000.0
+
+            ' Update displayed total audio length
+            m.totalLengthTimestamp.text = ticksToHuman(data.json.RunTimeTicks)
+        end if
+    end if
+end sub
+
+' Set poster image on screen
+sub setPosterImage(posterURL)
+    if isValid(posterURL)
+        if m.albumCover.uri <> posterURL
+            m.albumCover.uri = posterURL
+            m.screenSaverAlbumCover.uri = posterURL
+        end if
+    end if
+end sub
+
+' Set screen's title text
+sub setScreenTitle(json)
+    newTitle = ""
+    if isValid(json)
+        if isValid(json.AlbumArtist)
+            newTitle = json.AlbumArtist
+        end if
+        if isValid(json.AlbumArtist) and isValid(json.name)
+            newTitle = newTitle + " / "
+        end if
+        if isValid(json.name)
+            newTitle = newTitle + json.name
+        end if
+    end if
+
+    if m.top.overhangTitle <> newTitle
+        m.top.overhangTitle = newTitle
+    end if
+end sub
+
+' Populate on screen text variables
+sub setOnScreenTextValues(json)
+    if isValid(json)
+        if m.playlistTypeCount = 1
+            setTrackNumberDisplay()
+        end if
+
+        setFieldTextValue("artist", json.Artists[0])
+        setFieldTextValue("song", json.name)
+    end if
+end sub
+
+' Add backdrop image to screen
+sub setBackdropImage(data)
+    if isValid(data)
+        if m.backDrop.uri <> data
+            m.backDrop.uri = data
+        end if
+    end if
+end sub
+
+' Process key press events
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    ' Key bindings for remote control buttons
+    if press
+        ' If user presses key to turn off screensaver, don't do anything else with it
+        if screenSaverActive()
+            endScreenSaver()
+            return true
+        end if
+
+        if key = "play"
+            return playAction()
+        else if key = "back"
+            m.global.audioPlayer.control = "stop"
+            m.global.audioPlayer.loopMode = ""
+        else if key = "rewind"
+            return previousClicked()
+        else if key = "fastforward"
+            return nextClicked()
+        else if key = "left"
+            if m.global.queueManager.callFunc("getCount") = 1 then return false
+
+            if m.top.selectedButtonIndex > 0
+                m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
+                m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1
+            end if
+            return true
+        else if key = "right"
+            if m.global.queueManager.callFunc("getCount") = 1 then return false
+
+            m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
+            if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
+            return true
+        else if key = "OK"
+            if m.buttons.getChild(m.top.selectedButtonIndex).id = "play"
+                return playAction()
+            else if m.buttons.getChild(m.top.selectedButtonIndex).id = "previous"
+                return previousClicked()
+            else if m.buttons.getChild(m.top.selectedButtonIndex).id = "next"
+                return nextClicked()
+            else if m.buttons.getChild(m.top.selectedButtonIndex).id = "shuffle"
+                return shuffleClicked()
+            else if m.buttons.getChild(m.top.selectedButtonIndex).id = "loop"
+                return loopClicked()
+            end if
+        end if
+    end if
+
+    return false
+end function
+
+sub OnScreenHidden()
+    ' Write screen tracker for screensaver
+    WriteAsciiFile("tmp:/scene.temp", "")
+    MoveFile("tmp:/scene.temp", "tmp:/scene")
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_LoadScreenSaverTimeoutTask.bs.html b/docs/api/components_music_LoadScreenSaverTimeoutTask.bs.html new file mode 100644 index 000000000..78cd11cc6 --- /dev/null +++ b/docs/api/components_music_LoadScreenSaverTimeoutTask.bs.html @@ -0,0 +1,11 @@ +Source: components/music/LoadScreenSaverTimeoutTask.bs
On this page

components_music_LoadScreenSaverTimeoutTask.bs

sub init()
+    m.top.functionName = "getScreensaverTimeout"
+end sub
+
+sub getScreensaverTimeout()
+    appinfo = CreateObject("roAppManager")
+    m.top.content = appinfo.GetScreensaverTimeout() * 60
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_PlaylistView.bs.html b/docs/api/components_music_PlaylistView.bs.html new file mode 100644 index 000000000..35111a51a --- /dev/null +++ b/docs/api/components_music_PlaylistView.bs.html @@ -0,0 +1,163 @@ +Source: components/music/PlaylistView.bs
On this page

components_music_PlaylistView.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+    setupMainNode()
+
+    m.playAll = m.top.findNode("playAll")
+    m.albumCover = m.top.findNode("albumCover")
+    m.songList = m.top.findNode("songList")
+    m.infoGroup = m.top.FindNode("infoGroup")
+    m.songListRect = m.top.FindNode("songListRect")
+
+    m.songList.observeField("doneLoading", "onDoneLoading")
+
+    m.dscr = m.top.findNode("overview")
+    createDialogPallete()
+end sub
+
+sub setupMainNode()
+    main = m.top.findNode("toplevel")
+    main.translation = [96, 175]
+end sub
+
+' Set values for displayed values on screen
+sub pageContentChanged()
+    item = m.top.pageContent
+
+    setPosterImage(item.posterURL)
+    setScreenTitle(item.json)
+    setOnScreenTextValues(item.json)
+
+    ' Only 1 song shown, so hide Play Album button
+    if item.json.ChildCount = 1
+        m.playAll.visible = false
+    end if
+end sub
+
+' Set poster image on screen
+sub setPosterImage(posterURL)
+    if isValid(posterURL)
+        m.albumCover.uri = posterURL
+    end if
+end sub
+
+' Set screen's title text
+sub setScreenTitle(json)
+    newTitle = ""
+    if isValid(json)
+        if isValid(json.AlbumArtist)
+            newTitle = json.AlbumArtist
+        end if
+        if isValid(json.AlbumArtist) and isValid(json.name)
+            newTitle = newTitle + " / "
+        end if
+        if isValid(json.name)
+            newTitle = newTitle + json.name
+        end if
+    end if
+    m.top.overhangTitle = newTitle
+end sub
+
+' Adjust scene by removing overview node and showing more songs
+sub adjustScreenForNoOverview()
+    m.infoGroup.removeChild(m.dscr)
+    m.songListRect.height = 800
+    m.songList.numRows = 12
+end sub
+
+' Populate on screen text variables
+sub setOnScreenTextValues(json)
+    if isValid(json)
+        if isValid(json.overview) and json.overview <> ""
+            ' We have overview text
+            setFieldTextValue("overview", json.overview)
+        else
+            ' We don't have overview text
+            adjustScreenForNoOverview()
+        end if
+
+        setFieldTextValue("numberofsongs", stri(json.ChildCount) + " Tracks")
+
+        if type(json.ProductionYear) = "roInt"
+            setFieldTextValue("released", "Released " + stri(json.ProductionYear))
+        end if
+
+        if json.genres.count() > 0
+            setFieldTextValue("genres", json.genres.join(", "))
+        end if
+
+        if type(json.RunTimeTicks) = "LongInteger"
+            setFieldTextValue("runtime", stri(getMinutes(json.RunTimeTicks)) + " mins")
+        end if
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "options"
+        if m.dscr.isTextEllipsized
+            createFullDscrDlg()
+            return true
+        end if
+        return false
+    end if
+
+    if key = "right"
+        if m.playAll.hasFocus()
+            m.songList.setFocus(true)
+            return true
+        end if
+    else if key = "left" and m.songList.hasFocus()
+        if m.playAll.visible
+            m.playAll.setFocus(true)
+        else
+            return false
+        end if
+        return true
+    end if
+
+    return false
+end function
+
+sub createFullDscrDlg()
+    dlg = CreateObject("roSGNode", "OverviewDialog")
+    dlg.Title = tr("Press 'Back' to Close")
+    dlg.width = 1290
+    dlg.palette = m.dlgPalette
+    dlg.overview = [m.dscr.text]
+    m.fullDscrDlg = dlg
+    m.top.getScene().dialog = dlg
+    border = createObject("roSGNode", "Poster")
+    border.uri = "pkg:/images/hd_focul_9.png"
+    border.blendColor = "#c9c9c9ff"
+    border.width = dlg.width + 6
+    border.height = dlg.height + 6
+    border.translation = [dlg.translation[0] - 3, dlg.translation[1] - 3]
+    border.visible = true
+end sub
+
+sub createDialogPallete()
+    m.dlgPalette = createObject("roSGNode", "RSGPalette")
+    m.dlgPalette.colors = {
+        DialogBackgroundColor: "0x262828FF",
+        DialogItemColor: "0x00EF00FF",
+        DialogTextColor: "0xb0b0b0FF",
+        DialogFocusColor: "0xcececeFF",
+        DialogFocusItemColor: "0x202020FF",
+        DialogSecondaryTextColor: "0xf8f8f8ff",
+        DialogSecondaryItemColor: "0xcc7ecc4D",
+        DialogInputFieldColor: "0x80FF8080",
+        DialogKeyboardColor: "0x80FF804D",
+        DialogFootprintColor: "0x80FF804D"
+    }
+end sub
+
+sub onDoneLoading()
+    m.songList.unobservefield("doneLoading")
+    stopLoadingSpinner()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_music_SongItem.bs.html b/docs/api/components_music_SongItem.bs.html new file mode 100644 index 000000000..4e575928f --- /dev/null +++ b/docs/api/components_music_SongItem.bs.html @@ -0,0 +1,34 @@ +Source: components/music/SongItem.bs
On this page

components_music_SongItem.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.itemText = m.top.findNode("itemText")
+    m.trackNumber = m.top.findNode("trackNumber")
+    m.tracklength = m.top.findNode("tracklength")
+
+    m.defaultTextColor = m.itemText.color
+end sub
+
+sub itemContentChanged()
+    itemData = m.top.itemContent
+    if itemData = invalid then return
+    m.itemText.text = itemData.title
+    if itemData.trackNumber <> 0
+        m.trackNumber.text = itemData.trackNumber
+    end if
+    m.tracklength.text = ticksToHuman(itemData.length)
+end sub
+
+sub focusChanged()
+    if m.top.itemHasFocus
+        color = "#101010FF"
+    else
+        color = m.defaultTextColor
+    end if
+
+    m.itemText.color = color
+    m.trackNumber.color = color
+    m.tracklength.color = color
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_options_OptionNode.bs.html b/docs/api/components_options_OptionNode.bs.html new file mode 100644 index 000000000..3b51862de --- /dev/null +++ b/docs/api/components_options_OptionNode.bs.html @@ -0,0 +1,5 @@ +Source: components/options/OptionNode.bs
On this page

components_options_OptionNode.bs

sub init()
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_options_OptionsSlider.bs.html b/docs/api/components_options_OptionsSlider.bs.html new file mode 100644 index 000000000..a0ffc9fbc --- /dev/null +++ b/docs/api/components_options_OptionsSlider.bs.html @@ -0,0 +1,44 @@ +Source: components/options/OptionsSlider.bs
On this page

components_options_OptionsSlider.bs

sub init()
+    m.top.visible = false
+
+    panel = m.top.findNode("panel")
+    panel.panelSize = "small"
+    panel.leftPosition = 96
+    panel.focusable = true
+    panel.hasNextPanel = false
+    panel.leftOnly = true
+
+    list = m.top.findNode("panelList")
+
+    panel.list = list
+end sub
+
+sub setFields()
+    options = m.top.options
+    buttons = m.top.buttons
+    row = m.top.findNode("fieldList")
+
+    row.clear()
+    row.appendChildren(options)
+    row.appendChildren(buttons)
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "options" or key = "back"
+        m.top.visible = false
+        m.top.closeSidePanel = true
+        return true
+    else if key = "OK"
+        list = m.top.findNode("panelList")
+        data = list.content.getChild(list.itemFocused)
+        data.optionSelected = true
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_photos_LoadPhotoTask.bs.html b/docs/api/components_photos_LoadPhotoTask.bs.html new file mode 100644 index 000000000..b77000477 --- /dev/null +++ b/docs/api/components_photos_LoadPhotoTask.bs.html @@ -0,0 +1,31 @@ +Source: components/photos/LoadPhotoTask.bs
On this page

components_photos_LoadPhotoTask.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.functionName = "loadItems"
+end sub
+
+sub loadItems()
+    params = {
+        maxHeight: 1080,
+        maxWidth: 1920
+    }
+
+    if isValid(m.top.itemNodeContent)
+        item = m.top.itemNodeContent
+        m.top.results = ImageURL(item.Id, "Primary", params)
+    else if isValid(m.top.itemArrayContent)
+        item = m.top.itemArrayContent
+        m.top.results = ImageURL(item.Id, "Primary", params)
+    else
+        m.top.results = invalid
+    end if
+
+
+
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_photos_PhotoDetails.bs.html b/docs/api/components_photos_PhotoDetails.bs.html new file mode 100644 index 000000000..1bb379d4c --- /dev/null +++ b/docs/api/components_photos_PhotoDetails.bs.html @@ -0,0 +1,180 @@ +Source: components/photos/PhotoDetails.bs
On this page

components_photos_PhotoDetails.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.optionsAvailable = true
+    m.top.overhangVisible = false
+    m.slideshowTimer = m.top.findNode("slideshowTimer")
+    m.slideshowTimer.observeField("fire", "nextSlide")
+    m.status = m.top.findNode("status")
+    m.textBackground = m.top.findNode("background")
+    m.statusTimer = m.top.findNode("statusTimer")
+    m.statusTimer.observeField("fire", "statusUpdate")
+    m.slideshow = m.global.session.user.settings["photos.slideshow"]
+    m.random = m.global.session.user.settings["photos.random"]
+
+    m.showStatusAnimation = m.top.findNode("showStatusAnimation")
+    m.hideStatusAnimation = m.top.findNode("hideStatusAnimation")
+
+    itemContentChanged()
+end sub
+
+sub itemContentChanged()
+    if isValidToContinue(m.top.itemIndex)
+        m.LoadLibrariesTask = createObject("roSGNode", "LoadPhotoTask")
+        if isValid(m.top.itemsNode)
+            if isValid(m.top.itemsNode.content)
+                m.LoadLibrariesTask.itemNodeContent = m.top.itemsNode.content.getChild(m.top.itemIndex)
+            else if isValidAndNotEmpty(m.top.itemsNode.id)
+                m.LoadLibrariesTask.itemNodeContent = m.top.itemsNode
+            end if
+        else if isValid(m.top.itemsArray)
+            itemContent = m.top.itemsArray[m.top.itemIndex]
+            m.LoadLibrariesTask.itemArrayContent = itemContent
+        else
+            return
+        end if
+
+        m.LoadLibrariesTask.observeField("results", "onPhotoLoaded")
+        m.LoadLibrariesTask.control = "RUN"
+    end if
+end sub
+
+sub onPhotoLoaded()
+    stopLoadingSpinner()
+    if m.LoadLibrariesTask.results <> invalid
+        photo = m.top.findNode("photo")
+        photo.uri = m.LoadLibrariesTask.results
+
+        if m.slideshow = true or m.random = true
+            ' user has requested either a slideshow or random...
+            m.slideshowTimer.control = "start"
+        end if
+    else
+        'Show user error here (for example if it's not a supported image type)
+        message_dialog("This image type is not supported.")
+    end if
+end sub
+
+sub nextSlide()
+    m.slideshowTimer.control = "stop"
+
+    if m.slideshow = true
+        if isValidToContinue(m.top.itemIndex + 1)
+            m.top.itemIndex++
+            m.slideshowTimer.control = "start"
+        end if
+    else if m.random = true
+        index = invalid
+
+        if isValid(m.top.itemsNode)
+            if isValidAndNotEmpty(m.top.itemsNode.content)
+                index = rnd(m.top.itemsNode.content.getChildCount() - 1)
+            else
+                ' we're dealing with a single photo
+                return
+            end if
+        else if isValid(m.top.itemsArray)
+            if m.top.itemsArray.count() > 0
+                index = rnd(m.top.itemsArray.count() - 1)
+            end if
+        end if
+
+        if isValid(index) and isValidToContinue(index)
+            m.top.itemIndex = index
+            m.slideshowTimer.control = "start"
+        end if
+    end if
+end sub
+
+sub statusUpdate()
+    m.statusTimer.control = "stop"
+    m.hideStatusAnimation.control = "start"
+end sub
+
+' JFScreen hook.
+' Used to ensure tasks are stopped
+sub OnScreenHidden()
+    m.slideshowTimer.control = "stop"
+    m.statusTimer.control = "stop"
+end sub
+
+' isSlideshow component field has changed
+sub isSlideshowChanged()
+    m.slideshow = m.top.isSlideshow
+end sub
+
+' isRandom component field has changed
+sub isRandomChanged()
+    m.random = m.top.isRandom
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "right"
+        if isValidToContinue(m.top.itemIndex + 1)
+            m.slideshowTimer.control = "stop"
+            m.top.itemIndex++
+        end if
+        return true
+    end if
+
+    if key = "left"
+        if isValidToContinue(m.top.itemIndex - 1)
+            m.slideshowTimer.control = "stop"
+            m.top.itemIndex--
+        end if
+        return true
+    end if
+
+    if key = "play"
+        if m.slideshowTimer.control = "start"
+            ' stop the slideshow if the user hits "pause"
+            m.slideshowTimer.control = "stop"
+            m.status.text = tr("Slideshow Paused")
+            if m.textBackground.opacity = 0
+                m.showStatusAnimation.control = "start"
+            end if
+            m.statusTimer.control = "start"
+        else
+            ' start the slideshow if the user hits "play"
+            m.status.text = tr("Slideshow Resumed")
+            if m.textBackground.opacity = 0
+                m.showStatusAnimation.control = "start"
+            end if
+            m.slideshow = true
+            m.statusTimer.control = "start"
+            m.slideshowTimer.control = "start"
+        end if
+        return true
+    end if
+
+    if key = "options"
+        ' Options (random etc) is done on itemGrid
+        return true
+    end if
+
+    return false
+end function
+
+function isValidToContinue(index as integer)
+    if isValid(m.top.itemsNode)
+        if isValidAndNotEmpty(m.top.itemsNode.content)
+            if index >= 0 and index < m.top.itemsNode.content.getChildCount()
+                return true
+            end if
+        else if isValidAndNotEmpty(m.top.itemsNode) and index = 0
+            return true
+        end if
+    else if isValidAndNotEmpty(m.top.itemsArray)
+        if index >= 0 and index < m.top.itemsArray.count()
+            return true
+        end if
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_quickConnect_QuickConnect.bs.html b/docs/api/components_quickConnect_QuickConnect.bs.html new file mode 100644 index 000000000..95fa473a7 --- /dev/null +++ b/docs/api/components_quickConnect_QuickConnect.bs.html @@ -0,0 +1,20 @@ +Source: components/quickConnect/QuickConnect.bs
On this page

components_quickConnect_QuickConnect.bs

import "pkg:/source/api/userauth.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.functionName = "monitorQuickConnect"
+end sub
+
+sub monitorQuickConnect()
+    authenticated = checkQuickConnect(m.top.secret)
+
+    if authenticated = true
+        m.top.authenticated = 1
+    else
+        m.top.authenticated = -1
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_quickConnect_QuickConnectDialog.bs.html b/docs/api/components_quickConnect_QuickConnectDialog.bs.html new file mode 100644 index 000000000..2629125c2 --- /dev/null +++ b/docs/api/components_quickConnect_QuickConnectDialog.bs.html @@ -0,0 +1,74 @@ +Source: components/quickConnect/QuickConnectDialog.bs
On this page

components_quickConnect_QuickConnectDialog.bs

import "pkg:/source/api/userauth.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/session.bs"
+
+sub init()
+    m.quickConnectTimer = m.top.findNode("quickConnectTimer")
+    m.quickConnectTimer.observeField("fire", "quickConnectStatus")
+    m.quickConnectTimer.control = "start"
+    m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
+end sub
+
+sub quickConnectStatus()
+    m.quickConnectTimer.control = "stop"
+    m.checkTask = CreateObject("roSGNode", "QuickConnect")
+    m.checkTask.secret = m.top.quickConnectJson.secret
+    m.checkTask.observeField("authenticated", "OnAuthenticated")
+    m.checkTask.control = "run"
+end sub
+
+sub OnAuthenticated()
+    m.checkTask.unobserveField("authenticated")
+
+    ' Did we get the A-OK to authenticate?
+    authenticated = m.checkTask.authenticated
+    if authenticated < 0
+        ' Still waiting, check again in 3 seconds...
+        authenticated = 0
+        m.checkTask.observeField("authenticated", "OnAuthenticated")
+        m.quickConnectTimer.control = "start"
+    else if authenticated > 0
+        ' We've been given the go ahead, try to authenticate via Quick Connect...
+        authenticated = AuthenticateViaQuickConnect(m.top.quickConnectJson.secret)
+        if authenticated <> invalid and authenticated = true
+            currentUser = AboutMe()
+            session.user.Login(currentUser, m.top.saveCredentials)
+            session.user.LoadUserPreferences()
+            LoadUserAbilities()
+            m.top.close = true
+            m.top.authenticated = true
+        else
+            m.top.close = true
+            m.top.authenticated = false
+        end if
+    end if
+end sub
+
+sub quickConnectClosed()
+    m.quickConnectTimer.control = "stop"
+    if m.checkTask <> invalid
+        m.checkTask.unobserveField("authenticated")
+    end if
+    m.top.close = true
+end sub
+
+sub onButtonSelected()
+    ' only one button at the moment...
+    quickConnectClosed()
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    ' Note that "OK" does not get sent here, hence onButtonSelected() above.
+    if key = "back"
+        quickConnectClosed()
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_search_SearchResults.bs.html b/docs/api/components_search_SearchResults.bs.html new file mode 100644 index 000000000..8d267d3dc --- /dev/null +++ b/docs/api/components_search_SearchResults.bs.html @@ -0,0 +1,87 @@ +Source: components/search/SearchResults.bs
On this page

components_search_SearchResults.bs

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.optionsAvailable = false
+    m.searchSelect = m.top.findnode("searchSelect")
+    m.searchTask = CreateObject("roSGNode", "SearchTask")
+
+    'set label text
+    m.searchHelpText = m.top.findNode("SearchHelpText")
+    m.searchHelpText.text = tr("You can search for Titles, People, Live TV Channels and more")
+
+end sub
+
+sub searchMedias()
+    query = m.top.searchAlpha
+    'if user deletes the search string hide the spinner
+    if query.len() = 0
+        stopLoadingSpinner()
+    end if
+    'if search task is running and user selectes another letter stop the search and load the next letter
+    m.searchTask.control = "stop"
+    if query <> invalid and query <> ""
+        startLoadingSpinner(false)
+    end if
+    m.searchTask.observeField("results", "loadResults")
+    m.searchTask.query = query
+    m.top.overhangTitle = tr("Search") + ": " + query
+    m.searchTask.control = "RUN"
+
+end sub
+
+sub loadResults()
+    m.searchTask.unobserveField("results")
+
+    stopLoadingSpinner()
+    m.searchSelect.itemdata = m.searchTask.results
+    m.searchSelect.query = m.top.SearchAlpha
+    m.searchHelpText.visible = false
+    if m.searchTask.results.TotalRecordCount = 0
+        ' make sure focus is on the keyboard
+        if m.searchSelect.isinFocusChain()
+            m.searchAlphabox.setFocus(true)
+        end if
+        return
+    end if
+    m.searchAlphabox = m.top.findnode("searchResults")
+    m.searchAlphabox.translation = "[470, 85]"
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    m.searchAlphabox = m.top.findNode("search_Key")
+    if m.searchAlphabox.textEditBox.hasFocus()
+        m.searchAlphabox.textEditBox.translation = "[0, -150]"
+    else
+        m.searchAlphabox.textEditBox.translation = "[0, 0]"
+    end if
+
+    if key = "left" and m.searchSelect.isinFocusChain()
+        m.searchAlphabox.setFocus(true)
+        return true
+    else if key = "right" and m.searchSelect.content <> invalid and m.searchSelect.content.getChildCount() > 0
+        m.searchSelect.setFocus(true)
+        return true
+    else if key = "play" and m.searchSelect.isinFocusChain() and m.searchSelect.rowItemFocused.count() > 0
+        print "play was pressed from search results"
+        if m.searchSelect.rowItemFocused <> invalid
+            selectedContent = m.searchSelect.content.getChild(m.searchSelect.rowItemFocused[0])
+            if selectedContent <> invalid
+                selectedItem = selectedContent.getChild(m.searchSelect.rowItemFocused[1])
+                if selectedItem <> invalid
+                    m.top.quickPlayNode = selectedItem
+                    return true
+                end if
+            end if
+        end if
+    end if
+    return false
+
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_search_SearchRow.bs.html b/docs/api/components_search_SearchRow.bs.html new file mode 100644 index 000000000..18a9daf67 --- /dev/null +++ b/docs/api/components_search_SearchRow.bs.html @@ -0,0 +1,97 @@ +Source: components/search/SearchRow.bs
On this page

components_search_SearchRow.bs

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.itemComponentName = "ListPoster"
+    m.top.content = getData()
+
+    updateSize()
+
+    m.top.showRowLabel = [true]
+    m.top.rowLabelOffset = [0, 20]
+    m.top.showRowCounter = [true]
+
+    ' TODO - Define a failed to load image background
+    ' m.top.failedBitmapURI
+
+end sub
+
+sub updateSize()
+    ' In search results, rowSize only dictates how many are on screen at once
+    m.top.rowSize = 3
+
+    dimensions = m.top.getScene().currentDesignResolution
+
+    border = 50
+    m.top.translation = [border, border + 115]
+
+    textHeight = 80
+    itemWidth = (dimensions["width"] - border) / 6
+    itemHeight = itemWidth + (textHeight / 2.3)
+
+    m.top.itemSize = [1350, itemHeight] ' this is used for setting the row size
+    m.top.itemSpacing = [0, 105]
+
+    m.top.rowItemSize = [itemWidth, itemHeight]
+    m.top.rowItemSpacing = [0, 0]
+    m.top.numRows = 2
+    m.top.translation = "[12,18]"
+end sub
+
+function getData()
+    if m.top.itemData = invalid
+        data = CreateObject("roSGNode", "ContentNode")
+        return data
+    end if
+
+    itemData = m.top.itemData
+
+    ' todo - Or get the old data? I can't remember...
+    data = CreateObject("roSGNode", "ContentNode")
+    ' Do this to keep the ordering, AssociateArrays have no order
+    type_array = ["Movie", "Series", "TvChannel", "Episode", "MusicArtist", "MusicAlbum", "Audio", "Person", "PlaylistsFolder"]
+    content_types = {
+        "TvChannel": { "label": "Channels", "count": 0 },
+        "Movie": { "label": "Movies", "count": 0 },
+        "Series": { "label": "Shows", "count": 0 },
+        "Episode": { "label": "Episodes", "count": 0 },
+        "MusicArtist": { "label": "Artists", "count": 0 },
+        "MusicAlbum": { "label": "Albums", "count": 0 },
+        "Audio": { "label": "Songs", "count": 0 },
+        "Person": { "label": "People", "count": 0 },
+        "PlaylistsFolder": { "label": "Playlist", "count": 0 }
+    }
+
+    for each item in itemData.Items
+        if content_types[item.type] <> invalid
+            content_types[item.type].count += 1
+        end if
+    end for
+
+    for each ctype in type_array
+        content_type = content_types[ctype]
+        if content_type.count > 0
+            addRow(data, content_type.label, ctype)
+        end if
+    end for
+
+    m.top.content = data
+    return data
+end function
+
+sub addRow(data, title, type_filter)
+    itemData = m.top.itemData
+    row = data.CreateChild("ContentNode")
+    row.title = title
+    for each item in itemData.Items
+        if item.type = type_filter
+            row.appendChild(item)
+        end if
+    end for
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_search_SearchTask.bs.html b/docs/api/components_search_SearchTask.bs.html new file mode 100644 index 000000000..fe96ecb98 --- /dev/null +++ b/docs/api/components_search_SearchTask.bs.html @@ -0,0 +1,18 @@ +Source: components/search/SearchTask.bs
On this page

components_search_SearchTask.bs

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.functionName = "search"
+end sub
+
+sub search()
+    if m.top.query <> invalid and m.top.query <> ""
+        m.top.results = searchMedia(m.top.query)
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_section_section.bs.html b/docs/api/components_section_section.bs.html new file mode 100644 index 000000000..63bf8f97c --- /dev/null +++ b/docs/api/components_section_section.bs.html @@ -0,0 +1,100 @@ +Source: components/section/section.bs
On this page

components_section_section.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.showFromBottomAnimation = m.top.findNode("showFromBottomAnimation")
+    m.showFromBottomPosition = m.top.findNode("showFromBottomPosition")
+    m.showFromBottomOpacity = m.top.findNode("showFromBottomOpacity")
+
+    m.showFromTopAnimation = m.top.findNode("showFromTopAnimation")
+    m.showFromTopPosition = m.top.findNode("showFromTopPosition")
+    m.showFromTopOpacity = m.top.findNode("showFromTopOpacity")
+
+    m.scrollOffTopAnimation = m.top.findNode("scrollOffTopAnimation")
+    m.scrollOffTopPosition = m.top.findNode("scrollOffTopPosition")
+    m.scrollOffTopOpacity = m.top.findNode("scrollOffTopOpacity")
+
+    m.scrollOffBottomAnimation = m.top.findNode("scrollOffBottomAnimation")
+    m.scrollOffBottomPosition = m.top.findNode("scrollOffBottomPosition")
+    m.scrollOffBottomOpacity = m.top.findNode("scrollOffBottomOpacity")
+
+    m.scrollUpToOnDeckAnimation = m.top.findNode("scrollUpToOnDeckAnimation")
+    m.scrollUpToOnDeckPosition = m.top.findNode("scrollUpToOnDeckPosition")
+
+    m.scrollDownToOnDeckAnimation = m.top.findNode("scrollDownToOnDeckAnimation")
+    m.scrollDownToOnDeckPosition = m.top.findNode("scrollDownToOnDeckPosition")
+
+    m.scrollOffOnDeckAnimation = m.top.findNode("scrollOffOnDeckAnimation")
+    m.scrollOffOnDeckPosition = m.top.findNode("scrollOffOnDeckPosition")
+
+    m.top.observeField("translation", "onTranslationChange")
+    m.top.observeField("id", "onIDChange")
+    m.top.observeField("focusedChild", "onFocusChange")
+end sub
+
+sub onIDChange()
+    m.showFromBottomPosition.fieldToInterp = m.top.id + ".translation"
+    m.showFromBottomOpacity.fieldToInterp = m.top.id + ".opacity"
+
+    m.showFromTopPosition.fieldToInterp = m.top.id + ".translation"
+    m.showFromTopOpacity.fieldToInterp = m.top.id + ".opacity"
+
+    m.scrollOffTopPosition.fieldToInterp = m.top.id + ".translation"
+    m.scrollOffTopOpacity.fieldToInterp = m.top.id + ".opacity"
+
+    m.scrollOffBottomPosition.fieldToInterp = m.top.id + ".translation"
+    m.scrollOffBottomOpacity.fieldToInterp = m.top.id + ".opacity"
+
+    m.scrollUpToOnDeckPosition.fieldToInterp = m.top.id + ".translation"
+
+    m.scrollDownToOnDeckPosition.fieldToInterp = m.top.id + ".translation"
+
+    m.scrollOffOnDeckPosition.fieldToInterp = m.top.id + ".translation"
+end sub
+
+sub onTranslationChange()
+    m.startingPosition = m.top.translation
+    m.scrollOffBottomPosition.keyValue = "[[0, 0], [" + str(m.startingPosition[0]) + ", " + str(m.startingPosition[1]) + "]]"
+    m.top.unobserveField("translation")
+end sub
+
+sub showFromTop()
+    m.showFromTopAnimation.control = "start"
+end sub
+
+sub showFromBottom()
+    m.showFromBottomAnimation.control = "start"
+end sub
+
+sub scrollOffBottom()
+    m.scrollOffBottomAnimation.control = "start"
+end sub
+
+sub scrollOffTop()
+    m.scrollOffTopAnimation.control = "start"
+end sub
+
+sub scrollUpToOnDeck()
+    m.scrollUpToOnDeckAnimation.control = "start"
+end sub
+
+sub scrollDownToOnDeck()
+    m.scrollDownToOnDeckAnimation.control = "start"
+end sub
+
+sub scrollOffOnDeck()
+    m.scrollOffOnDeckAnimation.control = "start"
+end sub
+
+sub onFocusChange()
+    defaultFocusElement = m.top.findNode(m.top.defaultFocusID)
+
+    if isValid(defaultFocusElement)
+        defaultFocusElement.setFocus(m.top.isInFocusChain())
+        if isValid(defaultFocusElement.focus)
+            defaultFocusElement.focus = m.top.isInFocusChain()
+        end if
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_section_sectionScroller.bs.html b/docs/api/components_section_sectionScroller.bs.html new file mode 100644 index 000000000..7b9c72474 --- /dev/null +++ b/docs/api/components_section_sectionScroller.bs.html @@ -0,0 +1,63 @@ +Source: components/section/sectionScroller.bs
On this page

components_section_sectionScroller.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.previouslyDisplayedSection = 0
+end sub
+
+sub onFocusChange()
+    if m.top.focus
+        m.top.getChild(m.top.displayedIndex).setFocus(true)
+    end if
+end sub
+
+sub displayedIndexChanged()
+    if not m.top.affectsFocus then return
+
+    if m.top.displayedIndex < 0
+        return
+    end if
+
+    if m.top.displayedIndex > (m.top.getChildCount() - 1)
+        return
+    end if
+
+    m.top.getChild(m.previouslyDisplayedSection).setFocus(false)
+
+    displayedSection = m.top.getChild(m.top.displayedIndex)
+    displayedSection.setFocus(true)
+
+    onDeckSection = invalid
+    previouslyOnDeckSection = invalid
+
+    if m.top.displayedIndex + 1 <= (m.top.getChildCount() - 1)
+        onDeckSection = m.top.getChild(m.top.displayedIndex + 1)
+    end if
+
+    if m.top.displayedIndex + 2 <= (m.top.getChildCount() - 1)
+        previouslyOnDeckSection = m.top.getChild(m.top.displayedIndex + 2)
+    end if
+
+    ' Move sections either up or down depending on what index we're moving to
+    if m.top.displayedIndex > m.previouslyDisplayedSection
+        for i = m.previouslyDisplayedSection to m.top.displayedIndex - 1
+            m.top.getChild(i).callFunc("scrollOffTop")
+        end for
+
+        displayedSection.callFunc("showFromBottom")
+        if isValid(onDeckSection)
+            onDeckSection.callFunc("scrollUpToOnDeck")
+        end if
+    else if m.top.displayedIndex < m.previouslyDisplayedSection
+        m.top.getChild(m.top.displayedIndex + 1).callFunc("scrollDownToOnDeck")
+        displayedSection.callFunc("showFromTop")
+
+        if isValid(previouslyOnDeckSection)
+            previouslyOnDeckSection.callFunc("scrollOffOnDeck")
+        end if
+    end if
+
+    m.previouslyDisplayedSection = m.top.displayedIndex
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_settings_settings.bs.html b/docs/api/components_settings_settings.bs.html new file mode 100644 index 000000000..0abda18e4 --- /dev/null +++ b/docs/api/components_settings_settings.bs.html @@ -0,0 +1,257 @@ +Source: components/settings/settings.bs
On this page

components_settings_settings.bs

import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/misc.bs"
+import "pkg:/source/roku_modules/log/LogMixin.brs"
+' post device profile
+import "pkg:/source/utils/deviceCapabilities.bs"
+
+sub init()
+    m.log = log.Logger("Settings")
+    m.top.optionsAvailable = false
+
+    m.userLocation = []
+
+    m.settingsMenu = m.top.findNode("settingsMenu")
+    m.settingDetail = m.top.findNode("settingDetail")
+    m.settingDesc = m.top.findNode("settingDesc")
+    m.path = m.top.findNode("path")
+
+    m.boolSetting = m.top.findNode("boolSetting")
+    m.integerSetting = m.top.findNode("integerSetting")
+    m.radioSetting = m.top.findNode("radioSetting")
+
+    m.integerSetting.observeField("submit", "onKeyGridSubmit")
+    m.integerSetting.observeField("escape", "onKeyGridEscape")
+
+    m.settingsMenu.setFocus(true)
+    m.settingsMenu.observeField("itemFocused", "settingFocused")
+    m.settingsMenu.observeField("itemSelected", "settingSelected")
+
+    m.boolSetting.observeField("checkedItem", "boolSettingChanged")
+    m.radioSetting.observeField("checkedItem", "radioSettingChanged")
+
+    m.postTask = createObject("roSGNode", "PostTask")
+
+    ' Load Configuration Tree
+    m.configTree = GetConfigTree()
+    LoadMenu({ children: m.configTree })
+end sub
+
+sub onKeyGridSubmit()
+    selectedSetting = m.userLocation.peek().children[m.settingsMenu.itemFocused]
+    set_user_setting(selectedSetting.settingName, m.integerSetting.text)
+    m.settingsMenu.setFocus(true)
+end sub
+
+sub onKeyGridEscape()
+    if m.integerSetting.escape = "left" or m.integerSetting.escape = "back"
+        m.settingsMenu.setFocus(true)
+    end if
+end sub
+
+sub LoadMenu(configSection)
+    if configSection.children = invalid
+        ' Load parent menu
+        m.userLocation.pop()
+        configSection = m.userLocation.peek()
+    else
+        if m.userLocation.Count() > 0 then m.userLocation.peek().selectedIndex = m.settingsMenu.itemFocused
+        m.userLocation.push(configSection)
+    end if
+
+    result = CreateObject("roSGNode", "ContentNode")
+
+    for each item in configSection.children
+        listItem = result.CreateChild("ContentNode")
+        listItem.title = tr(item.title)
+        listItem.Description = tr(item.description)
+        listItem.id = item.id
+    end for
+
+    m.settingsMenu.content = result
+
+    if configSection.selectedIndex <> invalid and configSection.selectedIndex > -1
+        m.settingsMenu.jumpToItem = configSection.selectedIndex
+    end if
+
+    ' Set Path display
+    m.path.text = tr("Settings")
+    for each level in m.userLocation
+        if level.title <> invalid then m.path.text += " / " + tr(level.title)
+    end for
+end sub
+
+sub settingFocused()
+
+    selectedSetting = m.userLocation.peek().children[m.settingsMenu.itemFocused]
+    m.settingDesc.text = tr(selectedSetting.Description)
+    m.top.overhangTitle = tr(selectedSetting.Title)
+
+    ' Hide Settings
+    m.boolSetting.visible = false
+    m.integerSetting.visible = false
+    m.radioSetting.visible = false
+
+    if selectedSetting.type = invalid
+        return
+    else if selectedSetting.type = "bool"
+
+        m.boolSetting.visible = true
+
+        if m.global.session.user.settings[selectedSetting.settingName] = true
+            m.boolSetting.checkedItem = 1
+        else
+            m.boolSetting.checkedItem = 0
+        end if
+    else if selectedSetting.type = "integer"
+        integerValue = m.global.session.user.settings[selectedSetting.settingName].ToStr()
+        if isValid(integerValue)
+            m.integerSetting.text = integerValue
+        end if
+        m.integerSetting.visible = true
+    else if LCase(selectedSetting.type) = "radio"
+
+        selectedValue = m.global.session.user.settings[selectedSetting.settingName]
+
+        radioContent = CreateObject("roSGNode", "ContentNode")
+
+        itemIndex = 0
+        for each item in m.userLocation.peek().children[m.settingsMenu.itemFocused].options
+            listItem = radioContent.CreateChild("ContentNode")
+            listItem.title = tr(item.title)
+            listItem.id = item.id
+            if selectedValue = item.id
+                m.radioSetting.checkedItem = itemIndex
+            end if
+            itemIndex++
+        end for
+
+        m.radioSetting.content = radioContent
+
+        m.radioSetting.visible = true
+    else
+        m.log.warn("Unknown setting type", selectedSetting.type)
+    end if
+
+end sub
+
+
+sub settingSelected()
+
+    selectedItem = m.userLocation.peek().children[m.settingsMenu.itemFocused]
+
+    if selectedItem.type <> invalid ' Show setting
+        if selectedItem.type = "bool"
+            m.boolSetting.setFocus(true)
+        end if
+        if selectedItem.type = "integer"
+            m.integerSetting.setFocus(true)
+        end if
+        if (selectedItem.type) = "radio"
+            m.radioSetting.setFocus(true)
+        end if
+    else if selectedItem.children <> invalid and selectedItem.children.Count() > 0 ' Show sub menu
+        LoadMenu(selectedItem)
+        m.settingsMenu.setFocus(true)
+    else
+        return
+    end if
+
+    m.settingDesc.text = m.settingsMenu.content.GetChild(m.settingsMenu.itemFocused).Description
+
+end sub
+
+
+sub boolSettingChanged()
+    if m.boolSetting.focusedChild = invalid then return
+    selectedSetting = m.userLocation.peek().children[m.settingsMenu.itemFocused]
+
+    if m.boolSetting.checkedItem
+        session.user.settings.Save(selectedSetting.settingName, "true")
+        if Left(selectedSetting.settingName, 7) = "global."
+            ' global user setting
+            ' save to main registry block
+            set_setting(selectedSetting.settingName, "true")
+            ' setting specific triggers
+            if selectedSetting.settingName = "global.rememberme"
+                print "m.global.session.user.id=", m.global.session.user.id
+                set_setting("active_user", m.global.session.user.id)
+            end if
+        else
+            ' regular user setting
+            ' save to user specific registry block
+            set_user_setting(selectedSetting.settingName, "true")
+        end if
+    else
+        session.user.settings.Save(selectedSetting.settingName, "false")
+        if Left(selectedSetting.settingName, 7) = "global."
+            ' global user setting
+            ' save to main registry block
+            set_setting(selectedSetting.settingName, "false")
+            ' setting specific triggers
+            if selectedSetting.settingName = "global.rememberme"
+                unset_setting("active_user")
+            end if
+        else
+            ' regular user setting
+            ' save to user specific registry block
+            set_user_setting(selectedSetting.settingName, "false")
+        end if
+    end if
+end sub
+
+sub radioSettingChanged()
+    if m.radioSetting.focusedChild = invalid then return
+    selectedSetting = m.userLocation.peek().children[m.settingsMenu.itemFocused]
+    set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id)
+end sub
+
+' JFScreen hook that gets ran as needed.
+' Assumes settings were changed and they affect the device profile.
+' Posts a new device profile to the server using the task thread
+sub OnScreenHidden()
+    m.postTask.arrayData = getDeviceCapabilities()
+    m.postTask.apiUrl = "/Sessions/Capabilities/Full"
+    m.postTask.control = "RUN"
+    m.postTask.observeField("responseCode", "postFinished")
+end sub
+
+' Triggered by m.postTask after completing a post.
+' Empty the task data when finished.
+sub postFinished()
+    m.postTask.unobserveField("responseCode")
+    m.postTask.callFunc("empty")
+end sub
+
+' Returns true if any of the data entry forms are in focus
+function isFormInFocus() as boolean
+    if isValid(m.settingDetail.focusedChild) or m.radioSetting.hasFocus() or m.boolSetting.hasFocus() or m.integerSetting.hasFocus()
+        return true
+    end if
+    return false
+end function
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if (key = "back" or key = "left") and m.settingsMenu.focusedChild <> invalid and m.userLocation.Count() > 1
+        LoadMenu({})
+        return true
+    else if (key = "back" or key = "left") and isFormInFocus()
+        m.settingsMenu.setFocus(true)
+        return true
+    end if
+
+    if key = "options"
+        m.global.sceneManager.callFunc("popScene")
+        return true
+    end if
+
+    if key = "right"
+        settingSelected()
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tasks_PostTask.bs.html b/docs/api/components_tasks_PostTask.bs.html new file mode 100644 index 000000000..096b7673c --- /dev/null +++ b/docs/api/components_tasks_PostTask.bs.html @@ -0,0 +1,82 @@ +Source: components/tasks/PostTask.bs
On this page

components_tasks_PostTask.bs

import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.top.functionName = "postItems"
+end sub
+
+' Main function for PostTask.
+' Posts either an array of data
+' or a string of data to an API endpoint.
+' Saves the response information
+sub postItems()
+    if m.top.apiUrl = ""
+        print "ERROR in PostTask. Invalid API URL provided"
+        return
+    end if
+    if m.top.arrayData.count() > 0 and m.top.stringData = ""
+        print "PostTask Started - Posting array to " + m.top.apiUrl
+        req = APIRequest(m.top.apiUrl)
+        req.SetRequest("POST")
+        httpResponse = asyncPost(req, FormatJson(m.top.arrayData))
+        m.top.responseCode = httpResponse
+        print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr()
+    else if m.top.arrayData.count() = 0 and m.top.stringData <> ""
+        print "PostTask Started - Posting string(" + m.top.stringData + ") to " + m.top.apiUrl
+        req = APIRequest(m.top.apiUrl)
+        req.SetRequest("POST")
+        httpResponse = asyncPost(req, m.top.stringData)
+        m.top.responseCode = httpResponse
+        print "PostTask Finished. " + m.top.apiUrl + " Response = " + httpResponse.toStr()
+    else
+        print "ERROR processing data for PostTask"
+    end if
+end sub
+
+' Post data and wait for response code
+function asyncPost(req, data = "" as string) as integer
+    ' response code 0 means there was an error
+    respCode = 0
+
+    req.setMessagePort(CreateObject("roMessagePort"))
+    req.AddHeader("Content-Type", "application/json")
+    req.AsyncPostFromString(data)
+    ' wait up to m.top.timeoutSeconds for a response
+    ' NOTE: wait() uses milliseconds - multiply by 1000 to convert
+    resp = wait(m.top.timeoutSeconds * 1000, req.GetMessagePort())
+
+    respString = resp.GetString()
+    if isValidAndNotEmpty(respString)
+        m.top.responseBody = ParseJson(respString)
+        print "m.top.responseBody=", m.top.responseBody
+    end if
+
+    respCode = resp.GetResponseCode()
+    if respCode < 0
+        ' there was an unexpected error
+        m.top.failureReason = resp.GetFailureReason()
+    else if respCode >= 200 and respCode < 300
+        ' save response headers if they're available
+        m.top.responseHeaders = resp.GetResponseHeaders()
+    end if
+
+    return respCode
+end function
+
+' Revert PostTask to default state
+sub empty()
+    ' These should match the defaults set in PostTask.xml
+    m.top.apiUrl = ""
+    m.top.timeoutSeconds = 30
+
+    m.top.arrayData = {}
+    m.top.stringData = ""
+
+    m.top.responseCode = invalid
+    m.top.responseBody = {}
+    m.top.responseHeaders = {}
+    m.top.failureReason = ""
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVEpisodeRow.bs.html b/docs/api/components_tvshows_TVEpisodeRow.bs.html new file mode 100644 index 000000000..45d786457 --- /dev/null +++ b/docs/api/components_tvshows_TVEpisodeRow.bs.html @@ -0,0 +1,65 @@ +Source: components/tvshows/TVEpisodeRow.bs
On this page

components_tvshows_TVEpisodeRow.bs

sub init()
+    m.top.itemComponentName = "TVListDetails"
+    m.top.content = setData()
+
+    m.top.vertFocusAnimationStyle = "fixedFocusWrap"
+
+    m.top.showRowLabel = [false]
+
+    updateSize()
+
+    m.top.setFocus(true)
+end sub
+
+sub updateSize()
+    m.top.translation = [450, 180]
+
+    itemWidth = 1360
+    itemHeight = 300
+
+    m.top.visible = true
+
+    ' Size of the individual rows
+    m.top.itemSize = [itemWidth, itemHeight]
+
+    ' Spacing between Rows
+    m.top.itemSpacing = [0, 40]
+
+    ' Size of items in the row
+    m.top.rowItemSize = [itemWidth, itemHeight]
+    ' Spacing between items in the row
+    m.top.rowItemSpacing = [20, 0]
+end sub
+
+sub setupRows()
+    updateSize()
+    objects = m.top.objects
+    m.top.numRows = objects.items.count()
+    m.top.content = setData()
+end sub
+
+function setData()
+    data = CreateObject("roSGNode", "ContentNode")
+    if m.top.objects = invalid
+        ' Return an empty node just to return something; we'll update once we have data
+        return data
+    end if
+
+    for each item in m.top.objects.items
+        row = data.CreateChild("ContentNode")
+        row.appendChild(item)
+    end for
+
+    m.top.doneLoading = true
+
+    return data
+end function
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVEpisodeRowWithOptions.bs.html b/docs/api/components_tvshows_TVEpisodeRowWithOptions.bs.html new file mode 100644 index 000000000..131d80e4f --- /dev/null +++ b/docs/api/components_tvshows_TVEpisodeRowWithOptions.bs.html @@ -0,0 +1,126 @@ +Source: components/tvshows/TVEpisodeRowWithOptions.bs
On this page

components_tvshows_TVEpisodeRowWithOptions.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.rows = m.top.findNode("tvEpisodeRow")
+    m.tvListOptions = m.top.findNode("tvListOptions")
+
+    m.rows.observeField("doneLoading", "rowsDoneLoading")
+end sub
+
+sub setupRows()
+    objects = m.top.objects
+    m.rows.objects = objects
+end sub
+
+sub rowsDoneLoading()
+    m.top.doneLoading = true
+end sub
+
+' List of video versions to choose from
+sub SetUpVideoOptions(streams as object)
+    videos = []
+
+    for i = 0 to streams.Count() - 1
+        if LCase(streams[i].VideoType) = "videofile"
+            ' Create options for user to switch between video tracks
+            videos.push({
+                "Title": streams[i].Name,
+                "Description": tr("Video"),
+                "Selected": m.top.objects.items[m.currentSelected].selectedVideoStreamId = streams[i].id,
+                "StreamID": streams[i].id,
+                "video_codec": streams[i].mediaStreams[0].displayTitle
+            })
+        end if
+    end for
+
+    if videos.count() >= 1
+        options = {}
+        options.videos = videos
+        m.tvListOptions.options = options
+    end if
+end sub
+
+' List of audio tracks to choose from
+sub SetUpAudioOptions(streams as object)
+    tracks = []
+
+    for i = 0 to streams.Count() - 1
+        if streams[i].Type = "Audio"
+            tracks.push({
+                "Title": streams[i].displayTitle,
+                "Description": streams[i].Title,
+                "Selected": m.top.objects.items[m.currentSelected].selectedAudioStreamIndex = i,
+                "StreamIndex": i
+            })
+        end if
+    end for
+
+    if tracks.count() >= 1
+        options = {}
+        if isValid(m.tvListOptions.options) and isValid(m.tvListOptions.options.videos)
+            options.videos = m.tvListOptions.options.videos
+        end if
+        options.audios = tracks
+        m.tvListOptions.options = options
+    end if
+end sub
+
+sub audioOptionsClosed()
+    if m.currentSelected <> invalid
+        ' If the user opened the audio options, we report back even if they left the selection alone.
+        ' Otherwise, the users' lang peference from the server will take over.
+        ' To do this, we interpret anything other than "0" as the user opened the audio options.
+        m.top.objects.items[m.currentSelected].selectedAudioStreamIndex = m.tvListOptions.audioStreamIndex = 0 ? 1 : m.tvListOptions.audioStreamIndex
+    end if
+end sub
+
+sub videoOptionsClosed()
+    if m.tvListOptions.videoStreamId <> m.top.objects.items[m.currentSelected].selectedVideoStreamId
+        m.rows.objects.items[m.currentSelected].selectedVideoStreamId = m.tvListOptions.videoStreamId
+    end if
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "options" and isValid(m.rows.focusedChild) and isValid(m.rows.focusedChild.rowItemFocused)
+        m.currentSelected = m.rows.focusedChild.rowItemFocused[0]
+        mediaStreams = m.rows.objects.items[m.currentSelected].json.MediaStreams
+        mediaSources = m.rows.objects.items[m.currentSelected].json.MediaSources
+        if m.rows.objects.items[m.currentSelected].selectedVideoStreamId <> ""
+            for each source in mediaSources
+                if source.id = m.rows.objects.items[m.currentSelected].selectedVideoStreamId
+                    mediaStreams = source.MediaStreams
+                    exit for
+                end if
+            end for
+        end if
+        if isValid(mediaSources)
+            SetUpVideoOptions(mediaSources)
+        end if
+        if isValid(mediaStreams)
+            SetUpAudioOptions(mediaStreams)
+        end if
+        if isValid(m.tvListOptions.options)
+            m.tvListOptions.visible = true
+            m.tvListOptions.setFocus(true)
+        end if
+        return true
+    else if m.tvListOptions.visible = true and key = "back" or key = "options"
+        m.tvListOptions.setFocus(false)
+        m.tvListOptions.visible = false
+        m.rows.setFocus(true)
+        videoOptionsClosed()
+        audioOptionsClosed()
+        return true
+    else if key = "up" and m.rows.hasFocus() = false
+        m.rows.setFocus(true)
+    else if key = "down" and m.rows.hasFocus() = false
+        m.rows.setFocus(true)
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVEpisodes.bs.html b/docs/api/components_tvshows_TVEpisodes.bs.html new file mode 100644 index 000000000..66db331ed --- /dev/null +++ b/docs/api/components_tvshows_TVEpisodes.bs.html @@ -0,0 +1,138 @@ +Source: components/tvshows/TVEpisodes.bs
On this page

components_tvshows_TVEpisodes.bs

import "pkg:/source/api/Image.bs"
+import "pkg:/source/api/baserequest.bs"
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/misc.bs"
+import "pkg:/source/api/sdk.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+
+    m.rows = m.top.findNode("picker")
+    m.poster = m.top.findNode("seasonPoster")
+    m.shuffle = m.top.findNode("shuffle")
+    m.extras = m.top.findNode("extras")
+    m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
+
+    m.unplayedCount = m.top.findNode("unplayedCount")
+    m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
+
+    m.rows.observeField("doneLoading", "updateSeason")
+end sub
+
+sub setSeasonLoading()
+    m.top.overhangTitle = tr("Loading...")
+end sub
+
+' Updates the visibility of the Extras button based on if this season has any extra features
+sub setExtraButtonVisibility()
+    if isValid(m.top.extrasObjects) and isValidAndNotEmpty(m.top.extrasObjects.items)
+        m.extras.visible = true
+    end if
+end sub
+
+sub updateSeason()
+    if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
+        if isValid(m.top.seasonData) and isValid(m.top.seasonData.UserData) and isValid(m.top.seasonData.UserData.UnplayedItemCount)
+            if m.top.seasonData.UserData.UnplayedItemCount > 0
+                m.unplayedCount.visible = true
+                m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
+            end if
+        end if
+    end if
+
+    imgParams = { "maxHeight": 450, "maxWidth": 300 }
+    m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
+    m.shuffle.visible = true
+    m.top.overhangTitle = m.top.seasonData.SeriesName + " - " + m.top.seasonData.name
+end sub
+
+' get the currently focused item
+function getFocusedItem() as dynamic
+    if not isValid(m.top.focusedChild) or not isValid(m.top.focusedChild.focusedChild)
+        return invalid
+    end if
+
+    focusedChild = m.top.focusedChild.focusedChild
+    if not isValid(focusedChild.content) then return invalid
+    m.top.lastFocus = focusedChild
+
+    if isValidAndNotEmpty(focusedChild.rowItemFocused)
+        itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
+        if isValid(itemToPlay) and isValidAndNotEmpty(itemToPlay.id)
+            return itemToPlay
+        end if
+    end if
+
+    return invalid
+end function
+
+' Handle navigation input from the remote and act on it
+function onKeyEvent(key as string, press as boolean) as boolean
+    if key = "left" and m.tvEpisodeRow.hasFocus()
+        m.shuffle.setFocus(true)
+        return true
+    end if
+
+    if key = "right" and (m.shuffle.hasFocus() or m.extras.hasFocus())
+        m.tvEpisodeRow.setFocus(true)
+        return true
+    end if
+
+    if m.extras.visible and key = "up" and m.extras.hasFocus()
+        m.shuffle.setFocus(true)
+        return true
+    end if
+
+    if m.extras.visible and key = "down" and m.shuffle.hasFocus()
+        m.extras.setFocus(true)
+        return true
+    end if
+
+    if key = "OK"
+        if m.tvEpisodeRow.isInFocusChain()
+            focusedItem = getFocusedItem()
+            if isValid(focusedItem)
+                m.top.selectedItem = focusedItem
+            end if
+            return true
+        end if
+
+        if m.shuffle.hasFocus()
+            episodeList = m.rows.getChild(0).objects.items
+
+            for i = 0 to episodeList.count() - 1
+                j = Rnd(episodeList.count() - 1)
+                temp = episodeList[i]
+                episodeList[i] = episodeList[j]
+                episodeList[j] = temp
+            end for
+
+            m.global.queueManager.callFunc("set", episodeList)
+            m.global.queueManager.callFunc("playQueue")
+            return true
+        end if
+
+        if m.extras.visible and m.extras.hasFocus()
+            if LCase(m.extras.text.trim()) = LCase(tr("Extras"))
+                m.extras.text = tr("Episodes")
+                m.top.objects = m.top.extrasObjects
+            else
+                m.extras.text = tr("Extras")
+                m.top.objects = m.top.episodeObjects
+            end if
+        end if
+    end if
+
+    if key = "play"
+        focusedItem = getFocusedItem()
+        if isValid(focusedItem)
+            m.top.quickPlayNode = focusedItem
+        end if
+        return true
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVListDetails.bs.html b/docs/api/components_tvshows_TVListDetails.bs.html new file mode 100644 index 000000000..d029507f8 --- /dev/null +++ b/docs/api/components_tvshows_TVListDetails.bs.html @@ -0,0 +1,194 @@ +Source: components/tvshows/TVListDetails.bs
On this page

components_tvshows_TVListDetails.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.title = m.top.findNode("title")
+    m.title.text = tr("Loading...")
+    m.overview = m.top.findNode("overview")
+    m.poster = m.top.findNode("poster")
+
+    m.rating = m.top.findnode("rating")
+    m.infoBar = m.top.findnode("infoBar")
+    m.progressBackground = m.top.findNode("progressBackground")
+    m.progressBar = m.top.findnode("progressBar")
+    m.playedIndicator = m.top.findNode("playedIndicator")
+    m.checkmark = m.top.findNode("checkmark")
+    m.checkmark.font.size = 35
+
+    m.videoCodec = m.top.findNode("video_codec")
+end sub
+
+sub itemContentChanged()
+    item = m.top.itemContent
+    itemData = item.json
+
+    ' Set default video source if user hasn't selected one yet
+    if item.selectedVideoStreamId = "" and isValid(itemData.MediaSources)
+        item.selectedVideoStreamId = itemData.MediaSources[0].id
+    end if
+
+    if isValid(itemData.indexNumber)
+        indexNumber = itemData.indexNumber.toStr() + ". "
+    else
+        indexNumber = ""
+    end if
+    m.title.text = indexNumber + item.title
+    m.overview.text = item.overview
+
+    if isValid(itemData.PremiereDate)
+        airDate = CreateObject("roDateTime")
+        airDate.FromISO8601String(itemData.PremiereDate)
+        m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
+    end if
+
+    imageUrl = item.posterURL
+
+    if m.global.session.user.settings["ui.tvshows.blurunwatched"] = true
+        if itemData.lookup("Type") = "Episode"
+            if not itemData.userdata.played
+                imageUrl = imageUrl + "&blur=15"
+            end if
+        end if
+    end if
+
+    m.poster.uri = imageUrl
+
+    if type(itemData.RunTimeTicks) = "roInt" or type(itemData.RunTimeTicks) = "LongInteger"
+        runTime = getRuntime()
+        if runTime < 2
+            m.top.findNode("runtime").text = "1 min"
+        else
+            m.top.findNode("runtime").text = stri(runTime).trim() + " mins"
+        end if
+
+        if m.global.session.user.settings["ui.design.hideclock"] <> true
+            m.top.findNode("endtime").text = tr("Ends at %1").Replace("%1", getEndTime())
+        end if
+    end if
+
+    if m.global.session.user.settings["ui.tvshows.disableCommunityRating"] = false
+        if isValid(itemData.communityRating)
+            m.top.findNode("star").visible = true
+            m.top.findNode("communityRating").text = str(int(itemData.communityRating * 10) / 10)
+        else
+            m.top.findNode("star").visible = false
+        end if
+    else
+        m.rating.visible = false
+        m.infoBar.itemSpacings = [20, -25, 20, 20]
+    end if
+
+    ' Add checkmark in corner (if applicable)
+    if isValid(itemData.UserData) and isValid(itemData.UserData.Played) and itemData.UserData.Played = true
+        m.playedIndicator.visible = true
+    end if
+
+    ' Add progress bar on bottom (if applicable)
+    if isValid(itemData.UserData) and isValid(itemData.UserData.PlayedPercentage) and itemData.UserData.PlayedPercentage > 0
+        m.progressBackground.width = m.poster.width
+        m.progressBackground.visible = true
+        progressWidthInPixels = int(m.progressBackground.width * itemData.UserData.PlayedPercentage / 100)
+        m.progressBar.width = progressWidthInPixels
+        m.progressBar.visible = true
+    end if
+
+    ' Display current video_codec and check if there is more than one video to choose from...
+    m.videoCodec.visible = false
+    if isValid(itemData.MediaSources)
+        for i = 0 to itemData.MediaSources.Count() - 1
+            if item.selectedVideoStreamId = itemData.MediaSources[i].id and isValid(itemData.MediaSources[i].MediaStreams[0])
+                m.videoCodec.text = tr("Video") + ": " + itemData.MediaSources[i].MediaStreams[0].DisplayTitle
+                SetupAudioDisplay(itemData.MediaSources[i].MediaStreams, item.selectedAudioStreamIndex)
+                exit for
+            end if
+        end for
+        m.videoCodec.visible = true
+        DisplayVideoAvailable(itemData.MediaSources)
+    end if
+end sub
+
+' Display current audio_codec and check if there is more than one audio track to choose from...
+sub SetupAudioDisplay(mediaStreams as object, selectedAudioStreamIndex as integer)
+    audioIdx = invalid
+    if isValid(mediaStreams)
+        for i = 0 to mediaStreams.Count() - 1
+            if LCase(mediaStreams[i].Type) = "audio" and audioIdx = invalid
+                if selectedAudioStreamIndex > 0 and selectedAudioStreamIndex < mediaStreams.Count()
+                    audioIdx = selectedAudioStreamIndex
+                else
+                    audioIdx = i
+                end if
+                m.top.findNode("audio_codec").text = tr("Audio") + ": " + mediaStreams[audioIdx].DisplayTitle
+            end if
+            if isValid(audioIdx) then exit for
+        end for
+    end if
+
+    if isValid(audioIdx)
+        m.top.findNode("audio_codec").visible = true
+        DisplayAudioAvailable(mediaStreams)
+    else
+        m.top.findNode("audio_codec").visible = false
+    end if
+end sub
+
+' Adds "+N" (e.g. +1) if there is more than one video version to choose from
+sub DisplayVideoAvailable(streams as object)
+    count = 0
+    for i = 0 to streams.Count() - 1
+        if LCase(streams[i].VideoType) = "videofile"
+            count++
+        end if
+    end for
+
+    if count > 1
+        m.top.findnode("video_codec_count").text = "+" + stri(count - 1).trim()
+    end if
+end sub
+
+' Adds "+N" (e.g. +1) if there is more than one audio track to choose from
+sub DisplayAudioAvailable(streams as object)
+    count = 0
+    for i = 0 to streams.Count() - 1
+        if streams[i].Type = "Audio"
+            count++
+        end if
+    end for
+
+    if count > 1
+        m.top.findnode("audio_codec_count").text = "+" + stri(count - 1).trim()
+    end if
+end sub
+
+function getRuntime() as integer
+    itemData = m.top.itemContent.json
+
+    ' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
+    ' then 1/60 for seconds to minutess... 1/600,000,000
+    return int(itemData.RunTimeTicks / 600000000.0)
+end function
+
+function getEndTime() as string
+    itemData = m.top.itemContent.json
+    date = CreateObject("roDateTime")
+    duration_s = int(itemData.RunTimeTicks / 10000000.0)
+    date.fromSeconds(date.asSeconds() + duration_s)
+    date.toLocalTime()
+
+    return formatTime(date)
+end function
+
+sub focusChanged()
+    if m.top.itemHasFocus = true
+        ' text to speech for accessibility
+        if m.global.device.isAudioGuideEnabled = true
+            txt2Speech = CreateObject("roTextToSpeech")
+            txt2Speech.Flush()
+            txt2Speech.Say(m.title.text)
+            txt2Speech.Say(m.overview.text)
+        end if
+    end if
+end sub
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVListOptions.bs.html b/docs/api/components_tvshows_TVListOptions.bs.html new file mode 100644 index 000000000..ae42a862b --- /dev/null +++ b/docs/api/components_tvshows_TVListOptions.bs.html @@ -0,0 +1,157 @@ +Source: components/tvshows/TVListOptions.bs
On this page

components_tvshows_TVListOptions.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+
+    m.buttons = m.top.findNode("buttons")
+    m.buttons.buttons = [tr("Video"), tr("Audio")]
+    m.buttons.selectedIndex = 0
+    m.buttons.setFocus(true)
+
+    m.selectedItem = 0
+    m.selectedAudioIndex = 0
+    m.selectedVideoIndex = 0
+
+    m.menus = [m.top.findNode("videoMenu"), m.top.findNode("audioMenu")]
+
+    m.videoNames = []
+    m.audioNames = []
+
+    ' Set button colors to global
+    m.top.findNode("videoMenu").focusBitmapBlendColor = m.global.constants.colors.button
+    m.top.findNode("audioMenu").focusBitmapBlendColor = m.global.constants.colors.button
+
+    ' Animation
+    m.fadeAnim = m.top.findNode("fadeAnim")
+    m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
+    m.fadeInAnimOpacity = m.top.findNode("inOpacity")
+
+    m.buttons.observeField("focusedIndex", "buttonFocusChanged")
+    m.buttons.focusedIndex = m.selectedItem
+
+end sub
+
+sub optionsSet()
+    '  Videos Tab
+    if isValid(m.top.options.videos)
+        viewContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        selectedViewIndex = 0
+
+        for each view in m.top.options.videos
+            entry = viewContent.CreateChild("VideoTrackListData")
+            entry.title = view.Title
+            entry.description = view.Description
+            entry.streamId = view.streamId
+            entry.video_codec = view.video_codec
+            m.videoNames.push(view.Name)
+            if isValid(view.Selected) and view.Selected
+                selectedViewIndex = index
+                entry.selected = true
+                m.top.videoStreamId = view.streamId
+            end if
+            index = index + 1
+        end for
+
+        m.menus[0].content = viewContent
+        m.menus[0].jumpToItem = selectedViewIndex
+        m.menus[0].checkedItem = selectedViewIndex
+        m.selectedVideoIndex = selectedViewIndex
+    end if
+
+    '  audio Tab
+    if isValid(m.top.options.audios)
+        audioContent = CreateObject("roSGNode", "ContentNode")
+        index = 0
+        selectedAudioIndex = 0
+
+        for each audio in m.top.options.audios
+            entry = audioContent.CreateChild("AudioTrackListData")
+            entry.title = audio.Title
+            entry.description = audio.Description
+            entry.streamIndex = audio.StreamIndex
+            m.audioNames.push(audio.Name)
+            if isValid(audio.Selected) and audio.Selected
+                selectedAudioIndex = index
+                entry.selected = true
+                m.top.audioStreamIndex = audio.streamIndex
+            end if
+            index = index + 1
+        end for
+
+        m.menus[1].content = audioContent
+        m.menus[1].jumpToItem = selectedAudioIndex
+        m.menus[1].checkedItem = selectedAudioIndex
+        m.selectedAudioIndex = selectedAudioIndex
+    end if
+
+end sub
+
+' Switch menu shown when button focus changes
+sub buttonFocusChanged()
+    if m.buttons.focusedIndex = m.selectedItem then return
+    m.fadeOutAnimOpacity.fieldToInterp = m.menus[m.selectedItem].id + ".opacity"
+    m.fadeInAnimOpacity.fieldToInterp = m.menus[m.buttons.focusedIndex].id + ".opacity"
+    m.fadeAnim.control = "start"
+    m.selectedItem = m.buttons.focusedIndex
+end sub
+
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    if key = "down" or (key = "OK" and m.top.findNode("buttons").hasFocus())
+        m.top.findNode("buttons").setFocus(false)
+        m.menus[m.selectedItem].setFocus(true)
+        m.menus[m.selectedItem].drawFocusFeedback = true
+
+        'If user presses down from button menu, focus first item.  If OK, focus checked item
+        if key = "down"
+            m.menus[m.selectedItem].jumpToItem = 0
+        else
+            m.menus[m.selectedItem].jumpToItem = m.menus[m.selectedItem].itemSelected
+        end if
+
+        return true
+    else if key = "OK"
+        if m.menus[m.selectedItem].isInFocusChain()
+            selMenu = m.menus[m.selectedItem]
+            selIndex = selMenu.itemSelected
+
+            'Handle Videos menu
+            if m.selectedItem = 0
+                if m.selectedVideoIndex <> selIndex
+                    selMenu.content.GetChild(m.selectedVideoIndex).selected = false
+                    newSelection = selMenu.content.GetChild(selIndex)
+                    newSelection.selected = true
+                    m.selectedVideoIndex = selIndex
+                    m.top.videoStreamId = newSelection.streamId
+                end if
+                ' Then it is Audio options
+            else if m.selectedItem = 1
+                if m.selectedAudioIndex <> selIndex
+                    selMenu.content.GetChild(m.selectedAudioIndex).selected = false
+                    newSelection = selMenu.content.GetChild(selIndex)
+                    newSelection.selected = true
+                    m.selectedAudioIndex = selIndex
+                    m.top.audioStreamIndex = newSelection.streamIndex
+                end if
+            end if
+
+        end if
+        return true
+    else if key = "back" or key = "up"
+        if m.menus[m.selectedItem].isInFocusChain()
+            m.buttons.setFocus(true)
+            m.menus[m.selectedItem].drawFocusFeedback = false
+            return true
+        end if
+    else if key = "options"
+        m.menus[m.selectedItem].drawFocusFeedback = false
+        return false
+    end if
+
+    return false
+
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVSeasonRow.bs.html b/docs/api/components_tvshows_TVSeasonRow.bs.html new file mode 100644 index 000000000..4c537c573 --- /dev/null +++ b/docs/api/components_tvshows_TVSeasonRow.bs.html @@ -0,0 +1,51 @@ +Source: components/tvshows/TVSeasonRow.bs
On this page

components_tvshows_TVSeasonRow.bs

sub init()
+    m.top.itemComponentName = "ListPoster"
+    m.top.content = getData()
+
+    m.top.rowFocusAnimationStyle = "fixedFocusWrap"
+
+    m.top.showRowLabel = [false]
+    m.top.showRowCounter = [true]
+    m.top.rowLabelOffset = [0, 0]
+
+    updateSize()
+
+    m.top.setfocus(true)
+end sub
+
+sub updateSize()
+    itemWidth = 200
+    itemHeight = 320 ' width * 1.5 + text
+
+    m.top.visible = true
+
+    ' size of the whole row
+    m.top.itemSize = [1680, (itemHeight + 40)]
+    ' spacing between rows
+    m.top.itemSpacing = [0, 0]
+
+    ' size of the item in the row
+    m.top.rowItemSize = [itemWidth, itemHeight]
+    ' spacing between items in a row
+    m.top.rowItemSpacing = [0, 0]
+end sub
+
+function getData()
+    if m.top.TVSeasonData = invalid
+        data = CreateObject("roSGNode", "ContentNode")
+        return data
+    end if
+
+    seasonData = m.top.TVSeasonData
+    data = CreateObject("roSGNode", "ContentNode")
+    row = data.CreateChild("ContentNode")
+    row.title = "Seasons"
+    for each item in seasonData.items
+        row.appendChild(item)
+    end for
+    m.top.content = data
+    return data
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVShowDescription.bs.html b/docs/api/components_tvshows_TVShowDescription.bs.html new file mode 100644 index 000000000..bdaf5aebc --- /dev/null +++ b/docs/api/components_tvshows_TVShowDescription.bs.html @@ -0,0 +1,135 @@ +Source: components/tvshows/TVShowDescription.bs
On this page

components_tvshows_TVShowDescription.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    buttons = m.top.findNode("buttons")
+
+    buttons.iconUri = ""
+
+    for each button in buttons.getChildren(-1, 0)
+        button.maxWidth = 350
+        button.minWidth = 350
+    end for
+end sub
+
+sub itemContentChanged()
+    ' Updates video metadata
+    ' TODO - make things use item rather than itemData
+    item = m.top.itemContent
+    itemData = item.json
+
+    m.top.findNode("tvshowPoster").uri = m.top.itemContent.posterURL
+
+    ' Handle all "As Is" fields
+    setFieldText("title", itemData.name)
+    setFieldText("releaseYear", itemData.productionYear)
+    setFieldText("officialRating", itemData.officialRating)
+    setFieldText("communityRating", str(itemData.communityRating))
+    setFieldText("overview", itemData.overview)
+
+
+    if type(itemData.RunTimeTicks) = "LongInteger"
+        setFieldText("runtime", stri(getRuntime()) + " mins")
+    end if
+
+    setFieldText("history", getHistory())
+
+    if itemData.genres.count() > 0
+        setFieldText("genres", itemData.genres.join(", "))
+    end if
+    for each person in itemData.people
+        if person.type = "Director"
+            exit for
+        end if
+    end for
+    if itemData.taglines.count() > 0
+        setFieldText("tagline", itemData.taglines[0])
+    end if
+end sub
+
+sub setFieldText(field, value)
+    node = m.top.findNode(field)
+    if node = invalid or value = invalid then return
+
+    ' Handle non strings... Which _shouldn't_ happen, but hey
+    if type(value) = "roInt" or type(value) = "Integer"
+        value = str(value)
+    else if type(value) <> "roString" and type(value) <> "String"
+        value = ""
+    end if
+
+    node.text = value
+end sub
+
+function getRuntime() as integer
+    itemData = m.top.itemContent.json
+
+    ' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
+    ' then 1/60 for seconds to minutess... 1/600,000,000
+    return round(itemData.RunTimeTicks / 600000000.0)
+end function
+
+function getEndTime() as string
+    itemData = m.top.itemContent.json
+
+    date = CreateObject("roDateTime")
+    duration_s = int(itemData.RunTimeTicks / 10000000.0)
+    date.fromSeconds(date.asSeconds() + duration_s)
+    date.toLocalTime()
+
+    return formatTime(date)
+end function
+
+function getHistory() as string
+    itemData = m.top.itemContent.json
+    ' Aired Fridays at 9:30 PM on ABC (US)
+
+    airwords = invalid
+    studio = invalid
+    if itemData.status = "Ended"
+        verb = "Aired"
+    else
+        verb = "Airs"
+    end if
+
+    airdays = itemData.airdays
+    airtime = itemData.airtime
+    if airtime <> invalid and airdays.count() = 1
+        airwords = airdays[0] + " at " + airtime
+    end if
+
+    if itemData.studios.count() > 0
+        studio = itemData.studios[0].name
+    end if
+
+    if studio = invalid and airwords = invalid
+        return ""
+    end if
+
+    words = verb
+    if airwords <> invalid
+        words = words + " " + airwords
+    end if
+    if studio <> invalid
+        words = words + " on " + studio
+    end if
+
+    return words
+end function
+
+function round(f as float) as integer
+    ' BrightScript only has a "floor" round
+    ' This compares floor to floor + 1 to find which is closer
+    m = int(f)
+    n = m + 1
+    x = abs(f - m)
+    y = abs(f - n)
+    if y > x
+        return m
+    else
+        return n
+    end if
+end function
+
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_tvshows_TVShowDetails.bs.html b/docs/api/components_tvshows_TVShowDetails.bs.html new file mode 100644 index 000000000..c75d0d3cb --- /dev/null +++ b/docs/api/components_tvshows_TVShowDetails.bs.html @@ -0,0 +1,247 @@ +Source: components/tvshows/TVShowDetails.bs
On this page

components_tvshows_TVShowDetails.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    m.top.optionsAvailable = false
+    main = m.top.findNode("toplevel")
+    main.translation = [96, 175]
+    m.extrasSlider = m.top.findNode("tvSeasonExtras")
+    m.unplayedCount = m.top.findNode("unplayedCount")
+    m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
+    m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask")
+    m.Shuffle = m.top.findNode("Shuffle")
+    m.extrasSlider.visible = true
+    m.seasons = m.top.findNode("seasons")
+end sub
+
+sub itemContentChanged()
+    ' Updates video metadata
+    ' TODO - make things use item rather than itemData
+    item = m.top.itemContent
+    itemData = item.json
+
+    if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
+        if isValid(itemData.UserData) and isValid(itemData.UserData.UnplayedItemCount)
+            if itemData.UserData.UnplayedItemCount > 0
+                m.unplayedCount.visible = true
+                m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
+            end if
+        end if
+    end if
+
+    m.top.findNode("tvshowPoster").uri = m.top.itemContent.posterURL
+
+    ' Handle all "As Is" fields
+    m.top.overhangTitle = itemData.name
+
+    'Check production year, if invalid remove label
+    if isValid(itemData.productionYear)
+        setFieldText("releaseYear", itemData.productionYear)
+    else
+        m.top.findNode("main_group").removeChild(m.top.findNode("releaseYear"))
+    end if
+
+    'Check officialRating, if invalid remove label
+    if isValid(itemData.officialRating)
+        setFieldText("officialRating", itemData.officialRating)
+    else
+        m.top.findNode("main_group").removeChild(m.top.findNode("officialRating"))
+    end if
+
+    'Check communityRating, if invalid remove label
+    if isValid(itemData.communityRating)
+        m.top.findNode("star").visible = true
+        setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
+    else
+        m.top.findNode("main_group").removeChild(m.top.findNode("communityRating"))
+        m.top.findNode("main_group").removeChild(m.top.findNode("star"))
+        m.top.findNode("star").visible = false
+    end if
+
+    setFieldText("overview", itemData.overview)
+
+    m.Shuffle.visible = true
+
+    if type(itemData.RunTimeTicks) = "LongInteger"
+        setFieldText("runtime", stri(getRuntime()) + " mins")
+    end if
+
+    'History feild is set via the function getHistory()
+    setFieldText("history", getHistory())
+
+    'Check genres, if invalid remove label
+    if itemData.genres.count() > 0
+        setFieldText("genres", itemData.genres.join(", "))
+    else
+        m.top.findNode("main_group").removeChild(m.top.findNode("genres"))
+    end if
+
+    'We don't display Directors in the show page. Might want to remove this.
+    for each person in itemData.people
+        if person.type = "Director"
+            exit for
+        end if
+    end for
+    if itemData.taglines.count() > 0
+        setFieldText("tagline", itemData.taglines[0])
+    else
+        m.top.findNode("main_group").removeChild(m.top.findNode("tagline"))
+    end if
+end sub
+
+sub setFieldText(field, value)
+    node = m.top.findNode(field)
+    if node = invalid or value = invalid then return
+
+    ' Handle non strings... Which _shouldn't_ happen, but hey
+    if type(value) = "roInt" or type(value) = "Integer"
+        value = str(value).trim()
+    else if type(value) = "roFloat" or type(value) = "Float"
+        value = str(value).trim()
+    else if type(value) <> "roString" and type(value) <> "String"
+        value = ""
+    end if
+
+    node.text = value
+end sub
+
+function getRuntime() as integer
+    itemData = m.top.itemContent.json
+
+    ' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
+    ' then 1/60 for seconds to minutess... 1/600,000,000
+    return round(itemData.RunTimeTicks / 600000000.0)
+end function
+
+function getEndTime() as string
+    itemData = m.top.itemContent.json
+
+    date = CreateObject("roDateTime")
+    duration_s = int(itemData.RunTimeTicks / 10000000.0)
+    date.fromSeconds(date.asSeconds() + duration_s)
+    date.toLocalTime()
+
+    return formatTime(date)
+end function
+
+function getHistory() as string
+    itemData = m.top.itemContent.json
+    ' Aired Fridays at 9:30 PM on ABC (US)
+
+    airwords = invalid
+    studio = invalid
+    if itemData.status = "Ended"
+        verb = "Aired"
+    else
+        verb = "Airs"
+    end if
+
+    airdays = itemData.airdays
+    airtime = itemData.airtime
+    if isValid(airtime) and airdays.count() = 1
+        airwords = airdays[0] + " at " + airtime
+    end if
+
+    if itemData.studios.count() > 0
+        studio = itemData.studios[0].name
+    end if
+
+    if studio = invalid and airwords = invalid
+        m.top.findNode("main_group").removeChild(m.top.findNode("history"))
+        return ""
+    end if
+
+    words = verb
+    if isValid(airwords)
+        words = words + " " + airwords
+    end if
+    if isValid(studio)
+        words = words + " on " + studio
+    end if
+
+    return words
+end function
+
+function round(f as float) as integer
+    ' BrightScript only has a "floor" round
+    ' This compares floor to floor + 1 to find which is closer
+    m = int(f)
+    n = m + 1
+    x = abs(f - m)
+    y = abs(f - n)
+    if y > x
+        return m
+    else
+        return n
+    end if
+end function
+
+sub onShuffleEpisodeDataLoaded()
+    m.getShuffleEpisodesTask.unobserveField("data")
+    m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
+    m.global.queueManager.callFunc("playQueue")
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if key = "OK" or key = "play"
+        if m.Shuffle.hasFocus()
+            m.getShuffleEpisodesTask.showID = m.top.itemContent.id
+            m.getShuffleEpisodesTask.observeField("data", "onShuffleEpisodeDataLoaded")
+            m.getShuffleEpisodesTask.control = "RUN"
+            return true
+        end if
+    end if
+
+    if not press then return false
+
+    overview = m.top.findNode("overview")
+    topGrp = m.top.findNode("seasons")
+    bottomGrp = m.top.findNode("extrasGrid")
+
+    if key = "down" and overview.hasFocus()
+        m.Shuffle.setFocus(true)
+        return true
+    else if key = "down" and m.Shuffle.hasFocus()
+        topGrp.setFocus(true)
+        return true
+    else if key = "down" and topGrp.hasFocus()
+        bottomGrp.setFocus(true)
+        m.top.findNode("VertSlider").reverse = false
+        m.top.findNode("extrasFader").reverse = false
+        m.top.findNode("pplAnime").control = "start"
+        return true
+    else if key = "up" and bottomGrp.hasFocus()
+        if bottomGrp.itemFocused = 0
+            m.top.findNode("VertSlider").reverse = true
+            m.top.findNode("extrasFader").reverse = true
+            m.top.findNode("pplAnime").control = "start"
+            topGrp.setFocus(true)
+            return true
+        end if
+    else if key = "up" and topGrp.hasFocus()
+        m.Shuffle.setFocus(true)
+        return true
+    else if key = "up" and m.Shuffle.hasFocus()
+        overview.setFocus(true)
+        return true
+    else if key = "play" and m.seasons.hasFocus()
+        print "play was pressed from the seasons row"
+        if isValid(m.seasons.TVSeasonData) and isValid(m.seasons.TVSeasonData.Items)
+            itemFocused = m.seasons.rowItemFocused
+            m.top.quickPlayNode = m.seasons.TVSeasonData.Items[itemFocused[1]]
+            return true
+        end if
+    else if key = "play" and m.extrasSlider.isInFocusChain()
+        print "play was pressed from the extras grid"
+        extrasGrid = m.top.findNode("extrasGrid")
+        if extrasGrid.focusedItem <> invalid
+            m.top.quickPlayNode = extrasGrid.focusedItem
+            return true
+        end if
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_video_OSD.bs.html b/docs/api/components_video_OSD.bs.html new file mode 100644 index 000000000..3271e113c --- /dev/null +++ b/docs/api/components_video_OSD.bs.html @@ -0,0 +1,248 @@ +Source: components/video/OSD.bs
On this page

components_video_OSD.bs

import "pkg:/source/utils/misc.bs"
+
+sub init()
+    m.videoControls = m.top.findNode("videoControls")
+    m.optionControls = m.top.findNode("optionControls")
+
+    m.inactivityTimer = m.top.findNode("inactivityTimer")
+    m.itemTitle = m.top.findNode("itemTitle")
+    m.videoPlayPause = m.top.findNode("videoPlayPause")
+    m.videoPositionTime = m.top.findNode("videoPositionTime")
+    m.videoRemainingTime = m.top.findNode("videoRemainingTime")
+    m.progressBar = m.top.findNode("progressBar")
+    m.progressBarBackground = m.top.findNode("progressBarBackground")
+
+    m.top.observeField("visible", "onVisibleChanged")
+    m.top.observeField("hasFocus", "onFocusChanged")
+    m.top.observeField("progressPercentage", "onProgressPercentageChanged")
+    m.top.observeField("playbackState", "onPlaybackStateChanged")
+    m.top.observeField("itemTitleText", "onItemTitleTextChanged")
+
+    m.defaultButtonIndex = 1
+    m.focusedButtonIndex = 1
+
+    m.videoControls.buttonFocused = m.defaultButtonIndex
+    m.optionControls.buttonFocused = m.optionControls.getChildCount() - 1
+
+    m.videoControls.getChild(m.defaultButtonIndex).focus = true
+    m.deviceInfo = CreateObject("roDeviceInfo")
+end sub
+
+' onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param
+'
+sub onProgressPercentageChanged()
+    m.videoPositionTime.text = secondsToHuman(m.top.positionTime, true)
+    m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true)
+    m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage
+end sub
+
+' onPlaybackStateChanged: Handler for changes to m.top.playbackState param
+'
+sub onPlaybackStateChanged()
+    if LCase(m.top.playbackState) = "playing"
+        m.videoPlayPause.icon = "pkg:/images/icons/pause.png"
+        return
+    end if
+
+    m.videoPlayPause.icon = "pkg:/images/icons/play.png"
+end sub
+
+' onItemTitleTextChanged: Handler for changes to m.top.itemTitleText param.
+'
+sub onItemTitleTextChanged()
+    m.itemTitle.text = m.top.itemTitleText
+end sub
+
+' resetFocusToDefaultButton: Reset focus back to the default button
+'
+sub resetFocusToDefaultButton()
+    ' Remove focus from previously selected button
+    for each child in m.videoControls.getChildren(-1, 0)
+        if isValid(child.focus)
+            child.focus = false
+        end if
+    end for
+
+    for each child in m.optionControls.getChildren(-1, 0)
+        if isValid(child.focus)
+            child.focus = false
+        end if
+    end for
+
+    m.optionControls.setFocus(false)
+
+    ' Set focus back to the default button
+    m.videoControls.setFocus(true)
+    m.focusedButtonIndex = m.defaultButtonIndex
+    m.videoControls.getChild(m.defaultButtonIndex).focus = true
+    m.videoControls.buttonFocused = 1
+    m.optionControls.buttonFocused = m.optionControls.getChildCount() - 1
+end sub
+
+' onVisibleChanged: Handler for changes to the visibility of this menu.
+'
+sub onVisibleChanged()
+    if m.top.visible
+        resetFocusToDefaultButton()
+        m.inactivityTimer.observeField("fire", "inactiveCheck")
+        m.inactivityTimer.control = "start"
+        return
+    end if
+
+    m.inactivityTimer.unobserveField("fire")
+    m.inactivityTimer.control = "stop"
+end sub
+
+' onFocusChanged: Handler for changes to the focus of this menu.
+'
+sub onFocusChanged()
+    if m.top.hasfocus
+        focusedButton = m.optionControls.getChild(m.focusedButtonIndex)
+        if focusedButton.focus
+            m.optionControls.setFocus(true)
+            return
+        end if
+
+        m.videoControls.setFocus(true)
+    end if
+end sub
+
+' inactiveCheck: Checks if the time since last keypress is greater than or equal to the allowed inactive time of the menu.
+'
+sub inactiveCheck()
+    ' If user is currently seeing a dialog box, ignore inactive check
+    if m.global.sceneManager.callFunc("isDialogOpen")
+        return
+    end if
+
+    if m.deviceInfo.timeSinceLastKeypress() >= m.top.inactiveTimeout
+        m.top.action = "hide"
+    end if
+end sub
+
+' onButtonSelected: Handler for selection of buttons from the menu.
+'
+sub onButtonSelected()
+    if m.optionControls.isInFocusChain()
+        buttonGroup = m.optionControls
+    else
+        buttonGroup = m.videoControls
+    end if
+
+    selectedButton = buttonGroup.getChild(m.focusedButtonIndex)
+
+    if LCase(selectedButton.id) = "chapterlist"
+        m.top.showChapterList = not m.top.showChapterList
+    end if
+
+    m.top.action = selectedButton.id
+end sub
+
+function onKeyEvent(key as string, press as boolean) as boolean
+    if not press then return false
+
+    if key = "OK"
+        onButtonSelected()
+        return true
+    end if
+
+    if key = "play"
+        m.top.action = "videoplaypause"
+        return true
+    end if
+
+    if key = "right"
+        if m.optionControls.isInFocusChain()
+            buttonGroup = m.optionControls
+        else
+            buttonGroup = m.videoControls
+        end if
+
+        if m.focusedButtonIndex + 1 >= buttonGroup.getChildCount()
+            return true
+        end if
+
+        focusedButton = buttonGroup.getChild(m.focusedButtonIndex)
+        focusedButton.focus = false
+
+        ' Skip spacer elements until next button is found
+        for i = m.focusedButtonIndex + 1 to buttonGroup.getChildCount()
+            m.focusedButtonIndex = i
+            focusedButton = buttonGroup.getChild(m.focusedButtonIndex)
+
+            if isValid(focusedButton.focus)
+                buttonGroup.buttonFocused = m.focusedButtonIndex
+                focusedButton.focus = true
+                exit for
+            end if
+        end for
+
+        return true
+    end if
+
+    if key = "left"
+        if m.focusedButtonIndex = 0
+            return true
+        end if
+
+        if m.optionControls.isInFocusChain()
+            buttonGroup = m.optionControls
+        else
+            buttonGroup = m.videoControls
+        end if
+
+        focusedButton = buttonGroup.getChild(m.focusedButtonIndex)
+        focusedButton.focus = false
+
+        ' Skip spacer elements until next button is found
+        for i = m.focusedButtonIndex - 1 to 0 step -1
+            m.focusedButtonIndex = i
+            focusedButton = buttonGroup.getChild(m.focusedButtonIndex)
+
+            if isValid(focusedButton.focus)
+                buttonGroup.buttonFocused = m.focusedButtonIndex
+                focusedButton.focus = true
+                exit for
+            end if
+        end for
+
+        return true
+    end if
+
+    if key = "up"
+        if m.videoControls.isInFocusChain()
+            focusedButton = m.videoControls.getChild(m.focusedButtonIndex)
+            focusedButton.focus = false
+            m.videoControls.setFocus(false)
+
+            m.focusedButtonIndex = m.optionControls.buttonFocused
+            focusedButton = m.optionControls.getChild(m.focusedButtonIndex)
+            focusedButton.focus = true
+            m.optionControls.setFocus(true)
+        end if
+
+        return true
+    end if
+
+    if key = "down"
+        if m.optionControls.isInFocusChain()
+            focusedButton = m.optionControls.getChild(m.focusedButtonIndex)
+            focusedButton.focus = false
+            m.optionControls.setFocus(false)
+
+            m.focusedButtonIndex = m.videoControls.buttonFocused
+            focusedButton = m.videoControls.getChild(m.focusedButtonIndex)
+            focusedButton.focus = true
+            m.videoControls.setFocus(true)
+        end if
+
+        return true
+    end if
+
+    ' All other keys hide the menu
+    m.top.action = "hide"
+    return true
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/components_video_VideoPlayerView.bs.html b/docs/api/components_video_VideoPlayerView.bs.html new file mode 100644 index 000000000..29d8bd375 --- /dev/null +++ b/docs/api/components_video_VideoPlayerView.bs.html @@ -0,0 +1,699 @@ +Source: components/video/VideoPlayerView.bs
On this page

components_video_VideoPlayerView.bs

import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/config.bs"
+
+sub init()
+    ' Hide the overhang on init to prevent showing 2 clocks
+    m.top.getScene().findNode("overhang").visible = false
+    m.currentItem = m.global.queueManager.callFunc("getCurrentItem")
+
+    m.top.id = m.currentItem.id
+    m.top.seekMode = "accurate"
+
+    m.playbackEnum = {
+        null: -10
+    }
+
+    ' Load meta data
+    m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask")
+    m.LoadMetaDataTask.itemId = m.currentItem.id
+    m.LoadMetaDataTask.itemType = m.currentItem.type
+    m.LoadMetaDataTask.selectedAudioStreamIndex = m.currentItem.selectedAudioStreamIndex
+    m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
+    m.LoadMetaDataTask.control = "RUN"
+
+    m.chapterList = m.top.findNode("chapterList")
+    m.chapterMenu = m.top.findNode("chapterMenu")
+    m.chapterContent = m.top.findNode("chapterContent")
+    m.osd = m.top.findNode("osd")
+    m.osd.observeField("action", "onOSDAction")
+
+    m.playbackTimer = m.top.findNode("playbackTimer")
+    m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
+    m.top.observeField("state", "onState")
+    m.top.observeField("content", "onContentChange")
+    m.top.observeField("selectedSubtitle", "onSubtitleChange")
+
+    ' Custom Caption Function
+    m.top.observeField("allowCaptions", "onAllowCaptionsChange")
+
+    m.playbackTimer.observeField("fire", "ReportPlayback")
+    m.bufferPercentage = 0 ' Track whether content is being loaded
+    m.playReported = false
+    m.top.transcodeReasons = []
+    m.bufferCheckTimer.duration = 30
+
+    if 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
+
+    'Play Next Episode button
+    m.nextEpisodeButton = m.top.findNode("nextEpisode")
+    m.nextEpisodeButton.text = tr("Next Episode")
+    m.nextEpisodeButton.setFocus(false)
+    m.nextupbuttonseconds = m.global.session.user.settings["playback.nextupbuttonseconds"].ToInt()
+
+    m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
+    m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
+
+    m.checkedForNextEpisode = false
+    m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
+    m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
+
+    m.top.retrievingBar.filledBarBlendColor = m.global.constants.colors.blue
+    m.top.bufferingBar.filledBarBlendColor = m.global.constants.colors.blue
+    m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue
+end sub
+
+' handleChapterSkipAction: Handles user command to skip chapters in playing video
+'
+sub handleChapterSkipAction(action as string)
+    if not isValidAndNotEmpty(m.chapters) then return
+
+    currentChapter = getCurrentChapterIndex()
+
+    if action = "chapternext"
+        gotoChapter = currentChapter + 1
+        ' If there is no next chapter, exit
+        if gotoChapter > m.chapters.count() - 1 then return
+
+        m.top.seek = m.chapters[gotoChapter].StartPositionTicks / 10000000#
+        return
+    end if
+
+    if action = "chapterback"
+        gotoChapter = currentChapter - 1
+        ' If there is no previous chapter, restart current chapter
+        if gotoChapter < 0 then gotoChapter = 0
+
+        m.top.seek = m.chapters[gotoChapter].StartPositionTicks / 10000000#
+        return
+    end if
+end sub
+
+' handleHideAction: Handles action to hide OSD menu
+'
+' @param {boolean} resume - controls whether or not to resume video playback when sub is called
+'
+sub handleHideAction(resume as boolean)
+    m.osd.visible = false
+    m.chapterList.visible = false
+    m.osd.showChapterList = false
+    m.chapterList.setFocus(false)
+    m.osd.hasFocus = false
+    m.osd.setFocus(false)
+    m.top.setFocus(true)
+    if resume
+        m.top.control = "resume"
+    end if
+end sub
+
+' handleChapterListAction: Handles action to show chapter list
+'
+sub handleChapterListAction()
+    m.chapterList.visible = m.osd.showChapterList
+
+    if not m.chapterList.visible then return
+
+    m.chapterMenu.jumpToItem = getCurrentChapterIndex()
+
+    m.osd.hasFocus = false
+    m.osd.setFocus(false)
+    m.chapterMenu.setFocus(true)
+end sub
+
+' getCurrentChapterIndex: Finds current chapter index
+'
+' @return {integer} indicating index of current chapter within chapter data or 0 if chapter lookup fails
+'
+function getCurrentChapterIndex() as integer
+    if not isValidAndNotEmpty(m.chapters) then return 0
+
+    ' Give a 15 second buffer to compensate for user expectation and roku video position inaccuracy
+    ' Web client uses 10 seconds, but this wasn't enough for Roku in testing
+    currentPosition = m.top.position + 15
+    currentChapter = 0
+
+    for i = m.chapters.count() - 1 to 0 step -1
+        if currentPosition >= (m.chapters[i].StartPositionTicks / 10000000#)
+            currentChapter = i
+            exit for
+        end if
+    end for
+
+    return currentChapter
+end function
+
+' handleVideoPlayPauseAction: Handles action to either play or pause the video content
+'
+sub handleVideoPlayPauseAction()
+    ' If video is paused, resume it
+    if m.top.state = "paused"
+        handleHideAction(true)
+        return
+    end if
+
+    ' Pause video
+    m.top.control = "pause"
+end sub
+
+' handleShowSubtitleMenuAction: Handles action to show subtitle selection menu
+'
+sub handleShowSubtitleMenuAction()
+    m.top.selectSubtitlePressed = true
+end sub
+
+' handleShowVideoInfoPopupAction: Handles action to show video info popup
+'
+sub handleShowVideoInfoPopupAction()
+    m.top.selectPlaybackInfoPressed = true
+end sub
+
+' onOSDAction: Process action events from OSD to their respective handlers
+'
+sub onOSDAction()
+    action = LCase(m.osd.action)
+
+    if action = "hide"
+        handleHideAction(false)
+        return
+    end if
+
+    if action = "play"
+        handleHideAction(true)
+        return
+    end if
+
+    if action = "chapterback" or action = "chapternext"
+        handleChapterSkipAction(action)
+        return
+    end if
+
+    if action = "chapterlist"
+        handleChapterListAction()
+        return
+    end if
+
+    if action = "videoplaypause"
+        handleVideoPlayPauseAction()
+        return
+    end if
+
+    if action = "showsubtitlemenu"
+        handleShowSubtitleMenuAction()
+        return
+    end if
+
+    if action = "showvideoinfopopup"
+        handleShowVideoInfoPopupAction()
+        return
+    end if
+end sub
+
+' Only setup caption items if captions are allowed
+sub onAllowCaptionsChange()
+    if not m.top.allowCaptions then return
+
+    m.captionGroup = m.top.findNode("captionGroup")
+    m.captionGroup.createchildren(9, "LayoutGroup")
+    m.captionTask = createObject("roSGNode", "captionTask")
+    m.captionTask.observeField("currentCaption", "updateCaption")
+    m.captionTask.observeField("useThis", "checkCaptionMode")
+    m.top.observeField("subtitleTrack", "loadCaption")
+    m.top.observeField("globalCaptionMode", "toggleCaption")
+
+    if m.global.session.user.settings["playback.subs.custom"]
+        m.top.suppressCaptions = true
+        toggleCaption()
+    else
+        m.top.suppressCaptions = false
+    end if
+end sub
+
+' Set caption url to server subtitle track
+sub loadCaption()
+    if m.top.suppressCaptions
+        m.captionTask.url = m.top.subtitleTrack
+    end if
+end sub
+
+' Toggles visibility of custom subtitles and sets captionTask's player state
+sub toggleCaption()
+    m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
+    if LCase(m.top.globalCaptionMode) = "on"
+        m.captionTask.playerState = m.top.state + m.top.globalCaptionMode + "w"
+        m.captionGroup.visible = true
+    else
+        m.captionGroup.visible = false
+    end if
+end sub
+
+' Removes old subtitle lines and adds new subtitle lines
+sub updateCaption()
+    m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0)
+    m.captionGroup.appendChildren(m.captionTask.currentCaption)
+end sub
+
+' Event handler for when selectedSubtitle changes
+sub onSubtitleChange()
+    ' Save the current video position
+    m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
+
+    m.top.control = "stop"
+
+    m.LoadMetaDataTask.selectedSubtitleIndex = m.top.SelectedSubtitle
+    m.LoadMetaDataTask.itemId = m.currentItem.id
+    m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
+    m.LoadMetaDataTask.control = "RUN"
+end sub
+
+sub onPlaybackErrorDialogClosed(msg)
+    sourceNode = msg.getRoSGNode()
+    sourceNode.unobserveField("buttonSelected")
+    sourceNode.unobserveField("wasClosed")
+
+    m.global.sceneManager.callFunc("popScene")
+end sub
+
+sub onPlaybackErrorButtonSelected(msg)
+    sourceNode = msg.getRoSGNode()
+    sourceNode.close = true
+end sub
+
+sub showPlaybackErrorDialog(errorMessage as string)
+    dialog = createObject("roSGNode", "Dialog")
+    dialog.title = tr("Error During Playback")
+    dialog.buttons = [tr("OK")]
+    dialog.message = errorMessage
+    dialog.observeField("buttonSelected", "onPlaybackErrorButtonSelected")
+    dialog.observeField("wasClosed", "onPlaybackErrorDialogClosed")
+    m.top.getScene().dialog = dialog
+end sub
+
+sub onVideoContentLoaded()
+    m.LoadMetaDataTask.unobserveField("content")
+    m.LoadMetaDataTask.control = "STOP"
+
+    videoContent = m.LoadMetaDataTask.content
+    m.LoadMetaDataTask.content = []
+
+    stopLoadingSpinner()
+
+    ' If we have nothing to play, return to previous screen
+    if not isValid(videoContent)
+        showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server."))
+        return
+    end if
+
+    if not isValid(videoContent[0])
+        showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server."))
+        return
+    end if
+
+    m.top.content = videoContent[0].content
+    m.top.PlaySessionId = videoContent[0].PlaySessionId
+    m.top.videoId = videoContent[0].id
+    m.top.container = videoContent[0].container
+    m.top.mediaSourceId = videoContent[0].mediaSourceId
+    m.top.fullSubtitleData = videoContent[0].fullSubtitleData
+    m.top.audioIndex = videoContent[0].audioIndex
+    m.top.transcodeParams = videoContent[0].transcodeparams
+    m.chapters = videoContent[0].chapters
+
+    m.osd.itemTitleText = m.top.content.title
+
+    populateChapterMenu()
+
+    if m.LoadMetaDataTask.isIntro
+        ' Disable trackplay bar for intro videos
+        m.top.enableTrickPlay = false
+    else
+        ' Allow custom captions for non intro videos
+        m.top.allowCaptions = true
+    end if
+
+    if isValid(m.top.audioIndex)
+        m.top.audioTrack = (m.top.audioIndex + 1).toStr()
+    else
+        m.top.audioTrack = "2"
+    end if
+
+    m.top.setFocus(true)
+    m.top.control = "play"
+end sub
+
+' populateChapterMenu: ' Parse chapter data from API and appeand to chapter list menu
+'
+sub populateChapterMenu()
+    ' Clear any existing chapter list data
+    m.chapterContent.clear()
+
+    if not isValidAndNotEmpty(m.chapters)
+        chapterItem = CreateObject("roSGNode", "ContentNode")
+        chapterItem.title = tr("No Chapter Data Found")
+        chapterItem.playstart = m.playbackEnum.null
+        m.chapterContent.appendChild(chapterItem)
+        return
+    end if
+
+    for each chapter in m.chapters
+        chapterItem = CreateObject("roSGNode", "ContentNode")
+        chapterItem.title = chapter.Name
+        chapterItem.playstart = chapter.StartPositionTicks / 10000000#
+        m.chapterContent.appendChild(chapterItem)
+    end for
+end sub
+
+' Event handler for when video content field changes
+sub onContentChange()
+    if not isValid(m.top.content) then return
+
+    m.top.observeField("position", "onPositionChanged")
+end sub
+
+sub onNextEpisodeDataLoaded()
+    m.checkedForNextEpisode = true
+
+    m.top.observeField("position", "onPositionChanged")
+end sub
+
+'
+' Runs Next Episode button animation and sets focus to button
+sub showNextEpisodeButton()
+    if m.osd.visible 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 m.nextEpisodeButton.opacity = 0 and m.global.session.user.configuration.EnableNextEpisodeAutoPlay
+        m.nextEpisodeButton.visible = true
+        m.showNextEpisodeButtonAnimation.control = "start"
+        m.nextEpisodeButton.setFocus(true)
+    end if
+end sub
+
+'
+'Update count down text
+sub updateCount()
+    nextEpisodeCountdown = Int(m.top.duration - m.top.position)
+    if nextEpisodeCountdown < 0
+        nextEpisodeCountdown = 0
+    end if
+    m.nextEpisodeButton.text = tr("Next Episode") + " " + nextEpisodeCountdown.toStr()
+end sub
+
+'
+' Runs hide Next Episode button animation and sets focus back to video
+sub hideNextEpisodeButton()
+    m.hideNextEpisodeButtonAnimation.control = "start"
+    m.nextEpisodeButton.setFocus(false)
+    m.top.setFocus(true)
+end sub
+
+' Checks if we need to display the Next Episode button
+sub checkTimeToDisplayNextEpisode()
+    if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
+    if m.nextupbuttonseconds = 0 then return ' is the button disabled?
+
+    ' Don't show Next Episode button if trickPlayBar is visible
+    if m.top.trickPlayBar.visible then 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()
+        m.nextEpisodeButton.visible = false
+        m.nextEpisodeButton.setFocus(false)
+    end if
+end sub
+
+' When Video Player state changes
+sub onPositionChanged()
+
+    ' Pass video position data into OSD
+    m.osd.progressPercentage = m.top.position / m.top.duration
+    m.osd.positionTime = m.top.position
+    m.osd.remainingPositionTime = m.top.duration - m.top.position
+
+    if isValid(m.captionTask)
+        m.captionTask.currentPos = Int(m.top.position * 1000)
+    end if
+
+    ' Check if dialog is open
+    m.dialog = m.top.getScene().findNode("dialogBackground")
+    if not isValid(m.dialog)
+        ' Do not show Next Episode button for intro videos
+        if not m.LoadMetaDataTask.isIntro
+            checkTimeToDisplayNextEpisode()
+        end if
+    end if
+end sub
+
+'
+' When Video Player state changes
+sub onState(msg)
+    if isValid(m.captionTask)
+        m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
+    end if
+
+    ' Pass video state into OSD
+    m.osd.playbackState = m.top.state
+
+    ' When buffering, start timer to monitor buffering process
+    if m.top.state = "buffering" and m.bufferCheckTimer <> invalid
+
+        ' start timer
+        m.bufferCheckTimer.control = "start"
+        m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
+    else if m.top.state = "error"
+        if not m.playReported and m.top.transcodeAvailable
+            m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding
+        else
+            ' If an error was encountered, Display dialog
+            showPlaybackErrorDialog(tr("Error During Playback"))
+        end if
+
+        ' Stop playback and exit player
+        m.top.control = "stop"
+        m.top.backPressed = true
+    else if m.top.state = "playing"
+
+        ' Check if next episde is available
+        if isValid(m.top.showID)
+            if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
+                m.getNextEpisodeTask.showID = m.top.showID
+                m.getNextEpisodeTask.videoID = m.top.id
+                m.getNextEpisodeTask.control = "RUN"
+            end if
+        end if
+
+        if m.playReported = false
+            ReportPlayback("start")
+            m.playReported = true
+        else
+            ReportPlayback()
+        end if
+        m.playbackTimer.control = "start"
+    else if m.top.state = "paused"
+        m.playbackTimer.control = "stop"
+        ReportPlayback()
+    else if m.top.state = "stopped"
+        m.playbackTimer.control = "stop"
+        ReportPlayback("stop")
+        m.playReported = false
+    end if
+
+end sub
+
+'
+' Report playback to server
+sub ReportPlayback(state = "update" as string)
+
+    if m.top.position = invalid then return
+
+    params = {
+        "ItemId": m.top.id,
+        "PlaySessionId": m.top.PlaySessionId,
+        "PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
+        "IsPaused": (m.top.state = "paused")
+    }
+    if m.top.content.live
+        params.append({
+            "MediaSourceId": m.top.transcodeParams.MediaSourceId,
+            "LiveStreamId": m.top.transcodeParams.LiveStreamId
+        })
+        m.bufferCheckTimer.duration = 30
+    end if
+
+    ' Report playstate via worker task
+    playstateTask = m.global.playstateTask
+    playstateTask.setFields({ status: state, params: params })
+    playstateTask.control = "RUN"
+end sub
+
+'
+' Check the the buffering has not hung
+sub bufferCheck(msg)
+
+    if m.top.state <> "buffering"
+        ' If video is not buffering, stop timer
+        m.bufferCheckTimer.control = "stop"
+        m.bufferCheckTimer.unobserveField("fire")
+        return
+    end if
+    if m.top.bufferingStatus <> invalid
+
+        ' Check that the buffering percentage is increasing
+        if m.top.bufferingStatus["percentage"] > m.bufferPercentage
+            m.bufferPercentage = m.top.bufferingStatus["percentage"]
+        else if m.top.content.live = true
+            m.top.callFunc("refresh")
+        else
+            ' If buffering has stopped Display dialog
+            showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server."))
+
+            ' Stop playback and exit player
+            m.top.control = "stop"
+            m.top.backPressed = true
+        end if
+    end if
+
+end sub
+
+' stateAllowsOSD: Check if current video state allows showing the OSD
+'
+' @return {boolean} indicating if video state allows the OSD to show
+function stateAllowsOSD() as boolean
+    validStates = ["playing", "paused", "stopped"]
+    return inArray(validStates, m.top.state)
+end function
+
+function onKeyEvent(key as string, press as boolean) as boolean
+
+    ' Keypress handler while user is inside the chapter menu
+    if m.chapterMenu.hasFocus()
+        if not press then return false
+
+        if key = "OK"
+            focusedChapter = m.chapterMenu.itemFocused
+            selectedChapter = m.chapterMenu.content.getChild(focusedChapter)
+            seekTime = selectedChapter.playstart
+
+            ' Don't seek if user clicked on No Chapter Data
+            if seekTime = m.playbackEnum.null then return true
+
+            m.top.seek = seekTime
+            return true
+        end if
+
+        if key = "back" or key = "replay"
+            m.chapterList.visible = false
+            m.osd.showChapterList = false
+            m.chapterMenu.setFocus(false)
+            m.osd.hasFocus = true
+            m.osd.setFocus(true)
+            return true
+        end if
+
+        if key = "play"
+            handleVideoPlayPauseAction()
+        end if
+
+        return true
+    end if
+
+    if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
+        m.top.control = "stop"
+        m.top.state = "finished"
+        hideNextEpisodeButton()
+        return true
+    else
+        'Hide Next Episode Button
+        if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus()
+            m.nextEpisodeButton.opacity = 0
+            m.nextEpisodeButton.setFocus(false)
+            m.top.setFocus(true)
+        end if
+    end if
+
+    if not press then return false
+
+    if key = "down" and not m.top.trickPlayBar.visible
+        if not m.LoadMetaDataTask.isIntro
+            ' Don't allow user to open menu prior to video loading
+            if not stateAllowsOSD() then return true
+
+            m.osd.visible = true
+            m.osd.hasFocus = true
+            m.osd.setFocus(true)
+            return true
+        end if
+
+    else if key = "up" and not m.top.trickPlayBar.visible
+        if not m.LoadMetaDataTask.isIntro
+            ' Don't allow user to open menu prior to video loading
+            if not stateAllowsOSD() then return true
+
+            m.osd.visible = true
+            m.osd.hasFocus = true
+            m.osd.setFocus(true)
+            return true
+        end if
+
+    else if key = "OK" and not m.top.trickPlayBar.visible
+        if not m.LoadMetaDataTask.isIntro
+            ' Don't allow user to open menu prior to video loading
+            if not stateAllowsOSD() then return true
+
+            ' Show OSD, but don't pause video
+            m.osd.visible = true
+            m.osd.hasFocus = true
+            m.osd.setFocus(true)
+            return true
+        end if
+
+        return false
+    end if
+
+    ' Disable OSD for intro videos
+    if not m.LoadMetaDataTask.isIntro
+        if key = "play" and not m.top.trickPlayBar.visible
+
+            ' Don't allow user to open menu prior to video loading
+            if not stateAllowsOSD() then return true
+
+            ' If video is paused, resume it and don't show OSD
+            if m.top.state = "paused"
+                m.top.control = "resume"
+                return true
+            end if
+
+            ' Pause video and show OSD
+            m.top.control = "pause"
+            m.osd.visible = true
+            m.osd.hasFocus = true
+            m.osd.setFocus(true)
+            return true
+        end if
+    end if
+
+    if key = "back"
+        m.top.control = "stop"
+    end if
+
+    return false
+end function
+
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/data/search.json b/docs/api/data/search.json new file mode 100644 index 000000000..7c14b1f57 --- /dev/null +++ b/docs/api/data/search.json @@ -0,0 +1 @@ +{"list":[{"title":"module:AlbumData","link":"AlbumData"},{"title":"module:AlbumData.setFields","link":"setFields"},{"title":"module:AlbumGrid","link":"AlbumGrid"},{"title":"module:AlbumGrid.getData","link":"getData"},{"title":"module:AlbumGrid.init","link":"init"},{"title":"module:AlbumGrid.onKeyEvent","link":"onKeyEvent"},{"title":"module:AlbumTrackList","link":"AlbumTrackList"},{"title":"module:AlbumTrackList.getData","link":"getData"},{"title":"module:AlbumTrackList.init","link":"init"},{"title":"module:AlbumView","link":"AlbumView"},{"title":"module:AlbumView.adjustScreenForNoOverview","link":"adjustScreenForNoOverview","description":"

Adjust scene by removing overview node and showing more songs

"},{"title":"module:AlbumView.createDialogPallete","link":"createDialogPallete"},{"title":"module:AlbumView.createFullDscrDlg","link":"createFullDscrDlg"},{"title":"module:AlbumView.init","link":"init"},{"title":"module:AlbumView.onDoneLoading","link":"onDoneLoading"},{"title":"module:AlbumView.onKeyEvent","link":"onKeyEvent"},{"title":"module:AlbumView.pageContentChanged","link":"pageContentChanged","description":"

Set values for displayed values on screen

"},{"title":"module:AlbumView.setOnScreenTextValues","link":"setOnScreenTextValues","description":"

Populate on screen text variables

"},{"title":"module:AlbumView.setPosterImage","link":"setPosterImage","description":"

Set poster image on screen

"},{"title":"module:AlbumView.setScreenTitle","link":"setScreenTitle","description":"

Set screen's title text

"},{"title":"module:AlbumView.setupMainNode","link":"setupMainNode"},{"title":"module:Alpha","link":"Alpha"},{"title":"module:Alpha.init","link":"init"},{"title":"module:Alpha.onKeyEvent","link":"onKeyEvent"},{"title":"module:ArtistView","link":"ArtistView"},{"title":"module:ArtistView.OnScreenHidden","link":"OnScreenHidden"},{"title":"module:ArtistView.OnScreenShown","link":"OnScreenShown"},{"title":"module:ArtistView.artistOverviewChanged","link":"artistOverviewChanged","description":"

Event fired when page data is loaded

"},{"title":"module:ArtistView.createDialogPallete","link":"createDialogPallete"},{"title":"module:ArtistView.createFullDscrDlg","link":"createFullDscrDlg"},{"title":"module:ArtistView.dscrShowFocus","link":"dscrShowFocus"},{"title":"module:ArtistView.init","link":"init"},{"title":"module:ArtistView.onAlbumsData","link":"onAlbumsData"},{"title":"module:ArtistView.onAlbumsEscape","link":"onAlbumsEscape"},{"title":"module:ArtistView.onAppearsOnData","link":"onAppearsOnData"},{"title":"module:ArtistView.onAppearsOnEscape","link":"onAppearsOnEscape"},{"title":"module:ArtistView.onBackdropImageLoaded","link":"onBackdropImageLoaded"},{"title":"module:ArtistView.onButtonSelectedChange","link":"onButtonSelectedChange","description":"

Event handler when user selected a different playback button

"},{"title":"module:ArtistView.onEllipsisChanged","link":"onEllipsisChanged"},{"title":"module:ArtistView.onKeyEvent","link":"onKeyEvent"},{"title":"module:ArtistView.onSectionNavigationEscape","link":"onSectionNavigationEscape"},{"title":"module:ArtistView.onSectionNavigationSelected","link":"onSectionNavigationSelected"},{"title":"module:ArtistView.onSectionScrollerChange","link":"onSectionScrollerChange"},{"title":"module:ArtistView.pageContentChanged","link":"pageContentChanged","description":"

Event fired when page data is loaded

"},{"title":"module:ArtistView.setBackdropImage","link":"setBackdropImage","description":"

Add backdrop image to screen

"},{"title":"module:ArtistView.setPosterImage","link":"setPosterImage"},{"title":"module:ArtistView.setScreenTitle","link":"setScreenTitle"},{"title":"module:ArtistView.setupButtons","link":"setupButtons","description":"

Setup playback buttons, default to Play button selected

"},{"title":"module:ArtistView.setupMainNode","link":"setupMainNode"},{"title":"module:AudioPlayer","link":"AudioPlayer"},{"title":"module:AudioPlayer.ReportPlayback","link":"ReportPlayback","description":"

Report playback to server

"},{"title":"module:AudioPlayer.audioStateChanged","link":"audioStateChanged","description":"

State Change Event Handler

"},{"title":"module:AudioPlayer.init","link":"init"},{"title":"module:AudioPlayerView","link":"AudioPlayerView"},{"title":"module:AudioPlayerView.LoadNextSong","link":"LoadNextSong"},{"title":"module:AudioPlayerView.OnScreenHidden","link":"OnScreenHidden"},{"title":"module:AudioPlayerView.audioPositionChanged","link":"audioPositionChanged"},{"title":"module:AudioPlayerView.audioStateChanged","link":"audioStateChanged"},{"title":"module:AudioPlayerView.bufferPositionChanged","link":"bufferPositionChanged"},{"title":"module:AudioPlayerView.endScreenSaver","link":"endScreenSaver"},{"title":"module:AudioPlayerView.findCurrentSongIndex","link":"findCurrentSongIndex"},{"title":"module:AudioPlayerView.init","link":"init"},{"title":"module:AudioPlayerView.loadButtons","link":"loadButtons","description":"

If we have more and 1 song to play, fade in the next and previous controls

"},{"title":"module:AudioPlayerView.loopClicked","link":"loopClicked"},{"title":"module:AudioPlayerView.nextClicked","link":"nextClicked"},{"title":"module:AudioPlayerView.onAudioStreamLoaded","link":"onAudioStreamLoaded"},{"title":"module:AudioPlayerView.onBackdropImageLoaded","link":"onBackdropImageLoaded"},{"title":"module:AudioPlayerView.onButtonSelectedChange","link":"onButtonSelectedChange","description":"

Event handler when user selected a different playback button

"},{"title":"module:AudioPlayerView.onKeyEvent","link":"onKeyEvent","description":"

Process key press events

"},{"title":"module:AudioPlayerView.onMetaDataLoaded","link":"onMetaDataLoaded"},{"title":"module:AudioPlayerView.onScreensaverTimeoutLoaded","link":"onScreensaverTimeoutLoaded"},{"title":"module:AudioPlayerView.pageContentChanged","link":"pageContentChanged","description":"

Update values on screen when page content changes

"},{"title":"module:AudioPlayerView.playAction","link":"playAction"},{"title":"module:AudioPlayerView.previousClicked","link":"previousClicked"},{"title":"module:AudioPlayerView.resetLoopModeToDefault","link":"resetLoopModeToDefault"},{"title":"module:AudioPlayerView.screenSaverActive","link":"screenSaverActive"},{"title":"module:AudioPlayerView.setBackdropImage","link":"setBackdropImage","description":"

Add backdrop image to screen

"},{"title":"module:AudioPlayerView.setLoopButtonImage","link":"setLoopButtonImage"},{"title":"module:AudioPlayerView.setOnScreenTextValues","link":"setOnScreenTextValues","description":"

Populate on screen text variables

"},{"title":"module:AudioPlayerView.setPosterImage","link":"setPosterImage","description":"

Set poster image on screen

"},{"title":"module:AudioPlayerView.setScreenTitle","link":"setScreenTitle","description":"

Set screen's title text

"},{"title":"module:AudioPlayerView.setShuffleIconState","link":"setShuffleIconState"},{"title":"module:AudioPlayerView.setTrackNumberDisplay","link":"setTrackNumberDisplay"},{"title":"module:AudioPlayerView.setupAnimationTasks","link":"setupAnimationTasks"},{"title":"module:AudioPlayerView.setupAudioNode","link":"setupAudioNode","description":"

Creates audio node used to play song(s)

"},{"title":"module:AudioPlayerView.setupButtons","link":"setupButtons","description":"

Setup playback buttons, default to Play button selected

"},{"title":"module:AudioPlayerView.setupDataTasks","link":"setupDataTasks","description":"

Creates tasks to gather data needed to render Scene and play song

"},{"title":"module:AudioPlayerView.setupInfoNodes","link":"setupInfoNodes"},{"title":"module:AudioPlayerView.setupScreenSaver","link":"setupScreenSaver"},{"title":"module:AudioPlayerView.shuffleClicked","link":"shuffleClicked"},{"title":"module:AudioPlayerView.startScreenSaver","link":"startScreenSaver"},{"title":"module:AudioPlayerView.toggleShuffleEnabled","link":"toggleShuffleEnabled"},{"title":"module:AudioTrackListItem","link":"AudioTrackListItem"},{"title":"module:AudioTrackListItem.focusChanged","link":"focusChanged","description":"

Scroll description if focused

"},{"title":"module:AudioTrackListItem.init","link":"init"},{"title":"module:AudioTrackListItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:ButtonGroupHoriz","link":"ButtonGroupHoriz"},{"title":"module:ButtonGroupHoriz.init","link":"init"},{"title":"module:ButtonGroupHoriz.onKeyEvent","link":"onKeyEvent"},{"title":"module:ButtonGroupVert","link":"ButtonGroupVert"},{"title":"module:ButtonGroupVert.init","link":"init"},{"title":"module:ButtonGroupVert.onFocusButtonChanged","link":"onFocusButtonChanged"},{"title":"module:ButtonGroupVert.onFocusChanged","link":"onFocusChanged"},{"title":"module:ButtonGroupVert.onKeyEvent","link":"onKeyEvent"},{"title":"module:ChannelData","link":"ChannelData"},{"title":"module:ChannelData.setFields","link":"setFields"},{"title":"module:ChannelData.setPoster","link":"setPoster"},{"title":"module:Clock","link":"Clock"},{"title":"module:Clock.init","link":"init"},{"title":"module:Clock.onCurrentTimeTimerFire","link":"onCurrentTimeTimerFire","description":"

onCurrentTimeTimerFire: Code that runs every time the currentTimeTimer fires

"},{"title":"module:CollectionData","link":"CollectionData"},{"title":"module:CollectionData.setFields","link":"setFields"},{"title":"module:CollectionData.setPoster","link":"setPoster"},{"title":"module:ConfigData","link":"ConfigData"},{"title":"module:ConfigData.init","link":"init"},{"title":"module:ConfigItem","link":"ConfigItem"},{"title":"module:ConfigItem.init","link":"init"},{"title":"module:ConfigItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:ConfigItem.setColors","link":"setColors"},{"title":"module:ConfigList","link":"ConfigList"},{"title":"module:ConfigList.configListShowDialog","link":"configListShowDialog"},{"title":"module:ConfigList.dismiss_dialog","link":"dismiss_dialog"},{"title":"module:ConfigList.init","link":"init"},{"title":"module:ConfigList.onDialogButton","link":"onDialogButton"},{"title":"module:ConfigList.onItemSelected","link":"onItemSelected"},{"title":"module:ConfigList.setData","link":"setData"},{"title":"module:ExtrasItem","link":"ExtrasItem"},{"title":"module:ExtrasItem.focusChanged","link":"focusChanged"},{"title":"module:ExtrasItem.init","link":"init"},{"title":"module:ExtrasItem.showContent","link":"showContent"},{"title":"module:ExtrasRowList","link":"ExtrasRowList"},{"title":"module:ExtrasRowList.addRowSize","link":"addRowSize"},{"title":"module:ExtrasRowList.buildRow","link":"buildRow"},{"title":"module:ExtrasRowList.init","link":"init"},{"title":"module:ExtrasRowList.loadParts","link":"loadParts"},{"title":"module:ExtrasRowList.loadPersonVideos","link":"loadPersonVideos"},{"title":"module:ExtrasRowList.onAdditionalPartsLoaded","link":"onAdditionalPartsLoaded"},{"title":"module:ExtrasRowList.onLikeThisLoaded","link":"onLikeThisLoaded"},{"title":"module:ExtrasRowList.onMoviesLoaded","link":"onMoviesLoaded"},{"title":"module:ExtrasRowList.onPeopleLoaded","link":"onPeopleLoaded"},{"title":"module:ExtrasRowList.onRowItemFocused","link":"onRowItemFocused"},{"title":"module:ExtrasRowList.onRowItemSelected","link":"onRowItemSelected"},{"title":"module:ExtrasRowList.onSeriesLoaded","link":"onSeriesLoaded"},{"title":"module:ExtrasRowList.onShowsLoaded","link":"onShowsLoaded"},{"title":"module:ExtrasRowList.onSpecialFeaturesLoaded","link":"onSpecialFeaturesLoaded"},{"title":"module:ExtrasRowList.updateSize","link":"updateSize"},{"title":"module:FavoriteItemsTask","link":"FavoriteItemsTask"},{"title":"module:FavoriteItemsTask.init","link":"init"},{"title":"module:FavoriteItemsTask.setFavoriteStatus","link":"setFavoriteStatus"},{"title":"module:FolderData","link":"FolderData"},{"title":"module:FolderData.setFields","link":"setFields"},{"title":"module:FolderData.setPoster","link":"setPoster"},{"title":"module:GetFiltersTask","link":"GetFiltersTask"},{"title":"module:GetFiltersTask.getFiltersTask","link":"getFiltersTask"},{"title":"module:GetFiltersTask.init","link":"init"},{"title":"module:GetNextEpisodeTask","link":"GetNextEpisodeTask"},{"title":"module:GetNextEpisodeTask.getNextEpisodeTask","link":"getNextEpisodeTask"},{"title":"module:GetNextEpisodeTask.init","link":"init"},{"title":"module:GetPlaybackInfoTask","link":"GetPlaybackInfoTask"},{"title":"module:GetPlaybackInfoTask.GetTranscodingStats","link":"GetTranscodingStats"},{"title":"module:GetPlaybackInfoTask.ItemPostPlaybackInfo","link":"ItemPostPlaybackInfo"},{"title":"module:GetPlaybackInfoTask.getDisplayBitrate","link":"getDisplayBitrate"},{"title":"module:GetPlaybackInfoTask.getPlaybackInfoTask","link":"getPlaybackInfoTask","description":"

Returns an array of playback info to be displayed during playback.\nIn the future, with a custom playback info view, we can return an associated array.

"},{"title":"module:GetPlaybackInfoTask.havePlaybackInfo","link":"havePlaybackInfo"},{"title":"module:GetPlaybackInfoTask.init","link":"init"},{"title":"module:GetShuffleEpisodesTask","link":"GetShuffleEpisodesTask"},{"title":"module:GetShuffleEpisodesTask.getShuffleEpisodesTask","link":"getShuffleEpisodesTask"},{"title":"module:GetShuffleEpisodesTask.init","link":"init"},{"title":"module:GridItem","link":"GridItem"},{"title":"module:GridItem.focusChanged","link":"focusChanged","description":"

Display or hide title Visibility on focus change

"},{"title":"module:GridItem.focusChanging","link":"focusChanging","description":"

Use FocusPercent to animate scaling of Poser Image

"},{"title":"module:GridItem.init","link":"init"},{"title":"module:GridItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:GridItem.onPosterLoadStatusChanged","link":"onPosterLoadStatusChanged","description":"

Hide backdrop and text when poster loaded

"},{"title":"module:GridItemSmall","link":"GridItemSmall"},{"title":"module:GridItemSmall.focusChanged","link":"focusChanged"},{"title":"module:GridItemSmall.init","link":"init"},{"title":"module:GridItemSmall.itemContentChanged","link":"itemContentChanged"},{"title":"module:GridItemSmall.onPosterLoadStatusChanged","link":"onPosterLoadStatusChanged","description":"

Hide backdrop and text when poster loaded

"},{"title":"module:Home","link":"Home"},{"title":"module:Home.OnScreenShown","link":"OnScreenShown","description":"

JFScreen hook that gets ran as needed.\nUsed to update the focus, the state of the data, and tells the server about the device profile

"},{"title":"module:Home.init","link":"init"},{"title":"module:Home.loadLibraries","link":"loadLibraries"},{"title":"module:Home.postFinished","link":"postFinished","description":"

Triggered by m.postTask after completing a post.\nEmpty the task data when finished.

"},{"title":"module:Home.refresh","link":"refresh"},{"title":"module:HomeData","link":"HomeData"},{"title":"module:HomeData.setData","link":"setData"},{"title":"module:HomeItem","link":"HomeItem"},{"title":"module:HomeItem.drawProgressBar","link":"drawProgressBar","description":"

Draws and animates item progress bar

"},{"title":"module:HomeItem.focusChanged","link":"focusChanged","description":"

Enable title scrolling based on item Focus

"},{"title":"module:HomeItem.init","link":"init"},{"title":"module:HomeItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:HomeItem.onPosterLoadStatusChanged","link":"onPosterLoadStatusChanged","description":"

Hide backdrop and icon when poster loaded

"},{"title":"module:HomeRowItemSizes","link":"HomeRowItemSizes"},{"title":"module:HomeRows","link":"HomeRows"},{"title":"module:HomeRows.addHomeSection","link":"addHomeSection","description":"

addHomeSection: Adds a new home section to the home rows.

"},{"title":"module:HomeRows.createContinueWatchingRow","link":"createContinueWatchingRow","description":"

createContinueWatchingRow: Creates a row displaying items the user can continue watching

"},{"title":"module:HomeRows.createFavoritesRow","link":"createFavoritesRow","description":"

createFavoritesRow: Creates a row displaying items from the user's favorites list

"},{"title":"module:HomeRows.createLatestInRows","link":"createLatestInRows","description":"

createLatestInRows: Creates a row displaying latest items in each of the user's libraries

"},{"title":"module:HomeRows.createLibraryRow","link":"createLibraryRow","description":"

createLibraryRow: Creates a row displaying the user's libraries

"},{"title":"module:HomeRows.createLiveTVRow","link":"createLiveTVRow","description":"

createLiveTVRow: Creates a row displaying the live tv now on section

"},{"title":"module:HomeRows.createNextUpRow","link":"createNextUpRow","description":"

createNextUpRow: Creates a row displaying next episodes up to watch

"},{"title":"module:HomeRows.filterNodeArray","link":"filterNodeArray"},{"title":"module:HomeRows.getOriginalSectionIndex","link":"getOriginalSectionIndex","description":"

getOriginalSectionIndex: Gets the index of a section from user settings and adds count of currently known latest media sections

"},{"title":"module:HomeRows.getSectionIndex","link":"getSectionIndex","description":"

getSectionIndex: Returns index of requested section in home row content

"},{"title":"module:HomeRows.init","link":"init"},{"title":"module:HomeRows.itemSelected","link":"itemSelected"},{"title":"module:HomeRows.loadLibraries","link":"loadLibraries"},{"title":"module:HomeRows.loadingTimerComplete","link":"loadingTimerComplete","description":"

loadingTimerComplete: Event handler for when loading wait time has expired

"},{"title":"module:HomeRows.onKeyEvent","link":"onKeyEvent"},{"title":"module:HomeRows.onLibrariesLoaded","link":"onLibrariesLoaded","description":"

onLibrariesLoaded: Handler when LoadLibrariesTask returns data

"},{"title":"module:HomeRows.processUserSections","link":"processUserSections","description":"

processUserSections: Loop through user's chosen home section settings and generate the content for each row

"},{"title":"module:HomeRows.removeHomeSection","link":"removeHomeSection","description":"

removeHomeSection: Removes a home section from the home rows

"},{"title":"module:HomeRows.sectionExists","link":"sectionExists","description":"

sectionExists: Checks if passed section exists in home row content

"},{"title":"module:HomeRows.setRowItemSize","link":"setRowItemSize","description":"

setRowItemSize: Loops through all home sections and sets the correct item sizes per row

"},{"title":"module:HomeRows.updateContinueWatchingItems","link":"updateContinueWatchingItems","description":"

updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed

"},{"title":"module:HomeRows.updateFavoritesItems","link":"updateFavoritesItems","description":"

updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed

"},{"title":"module:HomeRows.updateHomeRows","link":"updateHomeRows","description":"

updateHomeRows: Update function exposed to outside components

"},{"title":"module:HomeRows.updateLatestItems","link":"updateLatestItems","description":"

updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed

"},{"title":"module:HomeRows.updateNextUpItems","link":"updateNextUpItems","description":"

updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed

"},{"title":"module:HomeRows.updateOnNowItems","link":"updateOnNowItems","description":"

updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed

"},{"title":"module:HomeRows.updateSize","link":"updateSize"},{"title":"module:IconButton","link":"IconButton"},{"title":"module:IconButton.init","link":"init"},{"title":"module:IconButton.onBackgroundChanged","link":"onBackgroundChanged"},{"title":"module:IconButton.onFocusChanged","link":"onFocusChanged"},{"title":"module:IconButton.onHeightChanged","link":"onHeightChanged"},{"title":"module:IconButton.onIconChanged","link":"onIconChanged"},{"title":"module:IconButton.onKeyEvent","link":"onKeyEvent"},{"title":"module:IconButton.onPaddingChanged","link":"onPaddingChanged"},{"title":"module:IconButton.onTextChanged","link":"onTextChanged"},{"title":"module:IconButton.onWidthChanged","link":"onWidthChanged"},{"title":"module:IconButton.setIconSize","link":"setIconSize"},{"title":"module:Image","link":"Image"},{"title":"module:Image.ImageURL","link":"ImageURL"},{"title":"module:Image.ItemImages","link":"ItemImages"},{"title":"module:Image.PosterImage","link":"PosterImage"},{"title":"module:Image.UserImageURL","link":"UserImageURL"},{"title":"module:ImageData","link":"ImageData"},{"title":"module:ImageData.setFields","link":"setFields"},{"title":"module:IntegerKeyboard","link":"IntegerKeyboard"},{"title":"module:IntegerKeyboard.init","link":"init"},{"title":"module:IntegerKeyboard.keySelected","link":"keySelected"},{"title":"module:IntegerKeyboard.onKeyEvent","link":"onKeyEvent"},{"title":"module:ItemGrid","link":"ItemGrid"},{"title":"module:ItemGrid.ItemDataLoaded","link":"ItemDataLoaded","description":"

Handle loaded data, and add to Grid

"},{"title":"module:ItemGrid.SetBackground","link":"SetBackground","description":"

Set Background Image

"},{"title":"module:ItemGrid.SetUpOptions","link":"SetUpOptions","description":"

Data to display when options button selected

"},{"title":"module:ItemGrid.getCollectionType","link":"getCollectionType","description":"

Return parent collection type

"},{"title":"module:ItemGrid.getItemFocused","link":"getItemFocused","description":"

Returns Focused Item

"},{"title":"module:ItemGrid.inStringArray","link":"inStringArray","description":"

Search string array for search value. Return if it's found

"},{"title":"module:ItemGrid.init","link":"init"},{"title":"module:ItemGrid.loadInitialItems","link":"loadInitialItems","description":"

Load initial set of Data

"},{"title":"module:ItemGrid.loadMoreData","link":"loadMoreData","description":"

Load next set of items

"},{"title":"module:ItemGrid.newBGLoaded","link":"newBGLoaded","description":"

When Image Loading Status changes

"},{"title":"module:ItemGrid.onChannelFocused","link":"onChannelFocused"},{"title":"module:ItemGrid.onChannelSelected","link":"onChannelSelected"},{"title":"module:ItemGrid.onGenreItemSelected","link":"onGenreItemSelected","description":"

Genre Item Selected

"},{"title":"module:ItemGrid.onItemFocused","link":"onItemFocused","description":"

Handle new item being focused

"},{"title":"module:ItemGrid.onItemSelected","link":"onItemSelected","description":"

Item Selected

"},{"title":"module:ItemGrid.onItemalphaSelected","link":"onItemalphaSelected"},{"title":"module:ItemGrid.onKeyEvent","link":"onKeyEvent"},{"title":"module:ItemGrid.onvoiceFilter","link":"onvoiceFilter"},{"title":"module:ItemGrid.optionsClosed","link":"optionsClosed","description":"

Check if options updated and any reloading required

"},{"title":"module:ItemGrid.setBoxsetsOptions","link":"setBoxsetsOptions","description":"

Set Boxset view, sort, and filter options

"},{"title":"module:ItemGrid.setDefaultOptions","link":"setDefaultOptions","description":"

Set Default view, sort, and filter options

"},{"title":"module:ItemGrid.setLiveTvOptions","link":"setLiveTvOptions","description":"

Set Live TV view, sort, and filter options

"},{"title":"module:ItemGrid.setMoviesOptions","link":"setMoviesOptions","description":"

Set Movies view, sort, and filter options

"},{"title":"module:ItemGrid.setMusicOptions","link":"setMusicOptions","description":"

Set Music view, sort, and filter options

"},{"title":"module:ItemGrid.setPhotoAlbumOptions","link":"setPhotoAlbumOptions","description":"

Set Photo Album view, sort, and filter options

"},{"title":"module:ItemGrid.setTvShowsOptions","link":"setTvShowsOptions","description":"

Set TV Show view, sort, and filter options

"},{"title":"module:ItemGrid.showTVGuide","link":"showTVGuide"},{"title":"module:ItemGrid.swapDone","link":"swapDone","description":"

Swap Complete

"},{"title":"module:ItemGrid.updateTitle","link":"updateTitle"},{"title":"module:ItemGridOptions","link":"ItemGridOptions"},{"title":"module:ItemGridOptions.buttonFocusChanged","link":"buttonFocusChanged","description":"

Switch menu shown when button focus changes

"},{"title":"module:ItemGridOptions.hideChecklist","link":"hideChecklist"},{"title":"module:ItemGridOptions.init","link":"init"},{"title":"module:ItemGridOptions.isFilterMenuDataValid","link":"isFilterMenuDataValid","description":"

Check if data for Filter Menu is valid

"},{"title":"module:ItemGridOptions.onFilterFocusChange","link":"onFilterFocusChange"},{"title":"module:ItemGridOptions.onKeyEvent","link":"onKeyEvent"},{"title":"module:ItemGridOptions.optionsSet","link":"optionsSet"},{"title":"module:ItemGridOptions.saveFavoriteItemSelected","link":"saveFavoriteItemSelected"},{"title":"module:ItemGridOptions.setHeartColor","link":"setHeartColor"},{"title":"module:ItemGridOptions.showChecklist","link":"showChecklist"},{"title":"module:ItemGridOptions.toggleFavorite","link":"toggleFavorite"},{"title":"module:Items","link":"Items"},{"title":"module:Items.AppearsOnList","link":"AppearsOnList","description":"

Get list of albums an artist appears on

"},{"title":"module:Items.ArtistOverview","link":"ArtistOverview","description":"

Music Artist Data

"},{"title":"module:Items.AudioItem","link":"AudioItem","description":"

Get Songs that are on an Album

"},{"title":"module:Items.AudioStream","link":"AudioStream"},{"title":"module:Items.BackdropImage","link":"BackdropImage"},{"title":"module:Items.CreateArtistMix","link":"CreateArtistMix","description":"

Get Instant Mix based on item

"},{"title":"module:Items.CreateInstantMix","link":"CreateInstantMix","description":"

Get Instant Mix based on item

"},{"title":"module:Items.GetIntroVideos","link":"GetIntroVideos","description":"

Get Intro Videos for an item

"},{"title":"module:Items.GetSongsByArtist","link":"GetSongsByArtist","description":"

Get list of songs belonging to an artist

"},{"title":"module:Items.ItemGetPlaybackInfo","link":"ItemGetPlaybackInfo"},{"title":"module:Items.ItemMetaData","link":"ItemMetaData","description":"

MetaData about an item

"},{"title":"module:Items.ItemPostPlaybackInfo","link":"ItemPostPlaybackInfo"},{"title":"module:Items.MusicAlbumList","link":"MusicAlbumList","description":"

Get list of albums belonging to an artist

"},{"title":"module:Items.MusicSongList","link":"MusicSongList","description":"

Get Songs that are on an Album

"},{"title":"module:Items.PlaylistItemList","link":"PlaylistItemList","description":"

Get Items that are under the provided item

"},{"title":"module:Items.TVEpisodeShuffleList","link":"TVEpisodeShuffleList"},{"title":"module:Items.TVEpisodes","link":"TVEpisodes","description":"

Returns a list of TV Shows for a given TV Show and season\nAccepts strings for the TV Show Id and the season Id

"},{"title":"module:Items.TVSeasonExtras","link":"TVSeasonExtras","description":"

Returns a list of extra features for a TV Show season\nAccepts a string that is a TV Show season id

"},{"title":"module:Items.TVSeasons","link":"TVSeasons","description":"

Seasons for a TV Show

"},{"title":"module:Items.searchMedia","link":"searchMedia","description":"

Search across all libraries

"},{"title":"module:Items.useTranscodeAudioStream","link":"useTranscodeAudioStream"},{"title":"module:JFButton","link":"JFButton"},{"title":"module:JFButton.init","link":"init"},{"title":"module:JFButton.onTextChanged","link":"onTextChanged","description":"

Whenever the text changes, pad both sides with whitespace so we can center the button text

"},{"title":"module:JFButtons","link":"JFButtons"},{"title":"module:JFButtons.focusChanged","link":"focusChanged","description":"

Change opacity of the highlighted menu item based on focus

"},{"title":"module:JFButtons.highlightSelected","link":"highlightSelected","description":"

Highlight selected menu option

"},{"title":"module:JFButtons.init","link":"init"},{"title":"module:JFButtons.onKeyEvent","link":"onKeyEvent"},{"title":"module:JFButtons.renderChanged","link":"renderChanged","description":"

When options are fully displayed, set focus and selected option

"},{"title":"module:JFButtons.selectedIndexChanged","link":"selectedIndexChanged","description":"

When Selected Index set, ensure it is the one Focused

"},{"title":"module:JFButtons.showButtons","link":"showButtons"},{"title":"module:JFButtons.updateButtons","link":"updateButtons"},{"title":"module:JFGroup","link":"JFGroup"},{"title":"module:JFGroup.init","link":"init"},{"title":"module:JFGroup.onKeyEvent","link":"onKeyEvent"},{"title":"module:JFMessageDialog","link":"JFMessageDialog"},{"title":"module:JFMessageDialog.init","link":"init"},{"title":"module:JFMessageDialog.onKeyEvent","link":"onKeyEvent"},{"title":"module:JFMessageDialog.redraw","link":"redraw"},{"title":"module:JFMessageDialog.updateMessage","link":"updateMessage"},{"title":"module:JFMessageDialog.updateOptions","link":"updateOptions"},{"title":"module:JFOverhang","link":"JFOverhang"},{"title":"module:JFOverhang.init","link":"init"},{"title":"module:JFOverhang.onVisibleChange","link":"onVisibleChange"},{"title":"module:JFOverhang.resetTime","link":"resetTime"},{"title":"module:JFOverhang.setClockVisibility","link":"setClockVisibility"},{"title":"module:JFOverhang.setRightSeperatorVisibility","link":"setRightSeperatorVisibility"},{"title":"module:JFOverhang.updateOptions","link":"updateOptions"},{"title":"module:JFOverhang.updateTime","link":"updateTime"},{"title":"module:JFOverhang.updateTimeDisplay","link":"updateTimeDisplay"},{"title":"module:JFOverhang.updateTitle","link":"updateTitle"},{"title":"module:JFOverhang.updateUser","link":"updateUser"},{"title":"module:JFScene","link":"JFScene"},{"title":"module:JFScene.disableRemoteChanged","link":"disableRemoteChanged","description":"

Triggered when the disableRemote boolean component field is changed

"},{"title":"module:JFScene.init","link":"init"},{"title":"module:JFScene.isLoadingChanged","link":"isLoadingChanged","description":"

Triggered when the isLoading boolean component field is changed

"},{"title":"module:JFScene.onKeyEvent","link":"onKeyEvent"},{"title":"module:JFScreen","link":"JFScreen"},{"title":"module:JFScreen.OnScreenHidden","link":"OnScreenHidden","description":"

Function called when the screen is hidden by the screen manager\nIt is expected that screens override this function if required,\nto handle focus any actions required on the screen being hidden

"},{"title":"module:JFScreen.OnScreenShown","link":"OnScreenShown","description":"

Function called when the screen is displayed by the screen manager\nIt is expected that screens override this function to handle focus\nmanagmenet and any other actions required on screen shown

"},{"title":"module:JFScreen.init","link":"init"},{"title":"module:JFServer","link":"JFServer"},{"title":"module:JFServer.init","link":"init"},{"title":"module:JFServer.itemContentChanged","link":"itemContentChanged"},{"title":"module:JFServer.onFocusPercentChange","link":"onFocusPercentChange"},{"title":"module:JFServer.setTextColor","link":"setTextColor"},{"title":"module:JFVideo","link":"JFVideo"},{"title":"module:JFVideo.ReportPlayback","link":"ReportPlayback","description":"

Report playback to server

"},{"title":"module:JFVideo.bufferCheck","link":"bufferCheck","description":"

Check the the buffering has not hung

"},{"title":"module:JFVideo.checkTimeToDisplayNextEpisode","link":"checkTimeToDisplayNextEpisode","description":"

Checks if we need to display the Next Episode button

"},{"title":"module:JFVideo.hideNextEpisodeButton","link":"hideNextEpisodeButton","description":"

Runs hide Next Episode button animation and sets focus back to video

"},{"title":"module:JFVideo.init","link":"init"},{"title":"module:JFVideo.loadCaption","link":"loadCaption"},{"title":"module:JFVideo.onAllowCaptionsChange","link":"onAllowCaptionsChange"},{"title":"module:JFVideo.onContentChange","link":"onContentChange","description":"

Event handler for when video content field changes

"},{"title":"module:JFVideo.onKeyEvent","link":"onKeyEvent"},{"title":"module:JFVideo.onNextEpisodeDataLoaded","link":"onNextEpisodeDataLoaded"},{"title":"module:JFVideo.onPositionChanged","link":"onPositionChanged","description":"

When Video Player state changes

"},{"title":"module:JFVideo.onState","link":"onState","description":"

When Video Player state changes

"},{"title":"module:JFVideo.showNextEpisodeButton","link":"showNextEpisodeButton","description":"

Runs Next Episode button animation and sets focus to button

"},{"title":"module:JFVideo.toggleCaption","link":"toggleCaption"},{"title":"module:JFVideo.updateCaption","link":"updateCaption"},{"title":"module:JFVideo.updateCount","link":"updateCount","description":"

Update count down text

"},{"title":"module:ListPoster","link":"ListPoster"},{"title":"module:ListPoster.focusChanged","link":"focusChanged","description":"

Enable title scrolling based on item Focus

"},{"title":"module:ListPoster.init","link":"init"},{"title":"module:ListPoster.itemContentChanged","link":"itemContentChanged"},{"title":"module:ListPoster.updateSize","link":"updateSize"},{"title":"module:LoadChannelsTask","link":"LoadChannelsTask"},{"title":"module:LoadChannelsTask.init","link":"init"},{"title":"module:LoadChannelsTask.loadChannels","link":"loadChannels"},{"title":"module:LoadItemsTask","link":"LoadItemsTask"},{"title":"module:LoadItemsTask.getPersonVideos","link":"getPersonVideos"},{"title":"module:LoadItemsTask.init","link":"init"},{"title":"module:LoadItemsTask.loadItems","link":"loadItems"},{"title":"module:LoadItemsTask2","link":"LoadItemsTask2"},{"title":"module:LoadItemsTask2.init","link":"init"},{"title":"module:LoadItemsTask2.loadItems","link":"loadItems"},{"title":"module:LoadPhotoTask","link":"LoadPhotoTask"},{"title":"module:LoadPhotoTask.init","link":"init"},{"title":"module:LoadPhotoTask.loadItems","link":"loadItems"},{"title":"module:LoadProgramDetailsTask","link":"LoadProgramDetailsTask"},{"title":"module:LoadProgramDetailsTask.init","link":"init"},{"title":"module:LoadProgramDetailsTask.loadProgramDetails","link":"loadProgramDetails"},{"title":"module:LoadScreenSaverTimeoutTask","link":"LoadScreenSaverTimeoutTask"},{"title":"module:LoadScreenSaverTimeoutTask.getScreensaverTimeout","link":"getScreensaverTimeout"},{"title":"module:LoadScreenSaverTimeoutTask.init","link":"init"},{"title":"module:LoadSheduleTask","link":"LoadSheduleTask"},{"title":"module:LoadSheduleTask.init","link":"init"},{"title":"module:LoadSheduleTask.loadSchedule","link":"loadSchedule"},{"title":"module:LoadVideoContentTask","link":"LoadVideoContentTask"},{"title":"module:LoadVideoContentTask.FindPreferredAudioStream","link":"FindPreferredAudioStream"},{"title":"module:LoadVideoContentTask.LoadItems_AddVideoContent","link":"LoadItems_AddVideoContent"},{"title":"module:LoadVideoContentTask.LoadItems_VideoPlayer","link":"LoadItems_VideoPlayer"},{"title":"module:LoadVideoContentTask.addNextEpisodesToQueue","link":"addNextEpisodesToQueue","description":"

Add next episodes to the playback queue

"},{"title":"module:LoadVideoContentTask.addSubtitlesToVideo","link":"addSubtitlesToVideo"},{"title":"module:LoadVideoContentTask.addVideoContentURL","link":"addVideoContentURL"},{"title":"module:LoadVideoContentTask.directPlaySupported","link":"directPlaySupported"},{"title":"module:LoadVideoContentTask.getContainerType","link":"getContainerType"},{"title":"module:LoadVideoContentTask.getSubtitleLanguages","link":"getSubtitleLanguages"},{"title":"module:LoadVideoContentTask.getTranscodeReasons","link":"getTranscodeReasons","description":"

Extract array of Transcode Reasons from the content URL

"},{"title":"module:LoadVideoContentTask.init","link":"init"},{"title":"module:LoadVideoContentTask.loadItems","link":"loadItems"},{"title":"module:LoadVideoContentTask.sortSubtitles","link":"sortSubtitles","description":"

Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top

"},{"title":"module:LoginScene","link":"LoginScene"},{"title":"module:LoginScene.init","link":"init"},{"title":"module:LoginScene.onKeyEvent","link":"onKeyEvent"},{"title":"module:Main","link":"Main"},{"title":"module:Main.Main","link":"Main"},{"title":"module:MovieData","link":"MovieData"},{"title":"module:MovieData.setContainer","link":"setContainer"},{"title":"module:MovieData.setFields","link":"setFields"},{"title":"module:MovieData.setPoster","link":"setPoster"},{"title":"module:MovieDetails","link":"MovieDetails"},{"title":"module:MovieDetails.OnScreenShown","link":"OnScreenShown"},{"title":"module:MovieDetails.SetDefaultAudioTrack","link":"SetDefaultAudioTrack"},{"title":"module:MovieDetails.SetUpAudioOptions","link":"SetUpAudioOptions"},{"title":"module:MovieDetails.SetUpVideoOptions","link":"SetUpVideoOptions"},{"title":"module:MovieDetails.audioOptionsClosed","link":"audioOptionsClosed","description":"

Check if options updated and any reloading required

"},{"title":"module:MovieDetails.getEndTime","link":"getEndTime"},{"title":"module:MovieDetails.getRuntime","link":"getRuntime"},{"title":"module:MovieDetails.init","link":"init"},{"title":"module:MovieDetails.itemContentChanged","link":"itemContentChanged"},{"title":"module:MovieDetails.onKeyEvent","link":"onKeyEvent"},{"title":"module:MovieDetails.round","link":"round"},{"title":"module:MovieDetails.setFavoriteColor","link":"setFavoriteColor"},{"title":"module:MovieDetails.setFieldText","link":"setFieldText"},{"title":"module:MovieDetails.setWatchedColor","link":"setWatchedColor"},{"title":"module:MovieDetails.trailerAvailableChanged","link":"trailerAvailableChanged"},{"title":"module:MovieDetails.videoOptionsClosed","link":"videoOptionsClosed","description":"

Check if options were updated and if any reloding is needed...

"},{"title":"module:MovieLibraryView","link":"MovieLibraryView"},{"title":"module:MovieLibraryView.FilterDataLoaded","link":"FilterDataLoaded","description":"

Logo Image Loaded Event Handler

"},{"title":"module:MovieLibraryView.ItemDataLoaded","link":"ItemDataLoaded","description":"

Handle loaded data, and add to Grid

"},{"title":"module:MovieLibraryView.LogoImageLoaded","link":"LogoImageLoaded","description":"

Logo Image Loaded Event Handler

"},{"title":"module:MovieLibraryView.OnScreenHidden","link":"OnScreenHidden"},{"title":"module:MovieLibraryView.OnScreenShown","link":"OnScreenShown"},{"title":"module:MovieLibraryView.SetBackground","link":"SetBackground","description":"

Set Background Image

"},{"title":"module:MovieLibraryView.SetName","link":"SetName","description":"

Set Selected Movie Name

"},{"title":"module:MovieLibraryView.SetOfficialRating","link":"SetOfficialRating","description":"

Set Selected Movie OfficialRating

"},{"title":"module:MovieLibraryView.SetOverview","link":"SetOverview","description":"

Set Selected Movie Overview

"},{"title":"module:MovieLibraryView.SetProductionYear","link":"SetProductionYear","description":"

Set Selected Movie ProductionYear

"},{"title":"module:MovieLibraryView.getCollectionType","link":"getCollectionType","description":"

Return parent collection type

"},{"title":"module:MovieLibraryView.getItemFocused","link":"getItemFocused","description":"

Returns Focused Item

"},{"title":"module:MovieLibraryView.getRuntime","link":"getRuntime"},{"title":"module:MovieLibraryView.inStringArray","link":"inStringArray","description":"

Search string array for search value. Return if it's found

"},{"title":"module:MovieLibraryView.init","link":"init"},{"title":"module:MovieLibraryView.loadInitialItems","link":"loadInitialItems","description":"

Load initial set of Data

"},{"title":"module:MovieLibraryView.loadMoreData","link":"loadMoreData","description":"

Load next set of items

"},{"title":"module:MovieLibraryView.newBGLoaded","link":"newBGLoaded","description":"

When Image Loading Status changes

"},{"title":"module:MovieLibraryView.onChannelSelected","link":"onChannelSelected"},{"title":"module:MovieLibraryView.onGenreItemSelected","link":"onGenreItemSelected","description":"

Genre Item Selected

"},{"title":"module:MovieLibraryView.onItemFocused","link":"onItemFocused","description":"

Handle new item being focused

"},{"title":"module:MovieLibraryView.onItemSelected","link":"onItemSelected","description":"

Item Selected

"},{"title":"module:MovieLibraryView.onItemalphaSelected","link":"onItemalphaSelected"},{"title":"module:MovieLibraryView.onKeyEvent","link":"onKeyEvent"},{"title":"module:MovieLibraryView.onvoiceFilter","link":"onvoiceFilter"},{"title":"module:MovieLibraryView.optionsClosed","link":"optionsClosed","description":"

Check if options updated and any reloading required

"},{"title":"module:MovieLibraryView.round","link":"round"},{"title":"module:MovieLibraryView.setFieldText","link":"setFieldText"},{"title":"module:MovieLibraryView.setMoviesOptions","link":"setMoviesOptions","description":"

Set Movies view, sort, and filter options

"},{"title":"module:MovieLibraryView.setSelectedOptions","link":"setSelectedOptions","description":"

Data to display when options button selected

"},{"title":"module:MovieLibraryView.setupNodes","link":"setupNodes"},{"title":"module:MovieLibraryView.swapDone","link":"swapDone","description":"

Swap Complete

"},{"title":"module:MovieOptions","link":"MovieOptions"},{"title":"module:MovieOptions.buttonFocusChanged","link":"buttonFocusChanged","description":"

Switch menu shown when button focus changes

"},{"title":"module:MovieOptions.init","link":"init"},{"title":"module:MovieOptions.onKeyEvent","link":"onKeyEvent"},{"title":"module:MovieOptions.optionsSet","link":"optionsSet"},{"title":"module:MusicAlbumData","link":"MusicAlbumData"},{"title":"module:MusicAlbumData.setFields","link":"setFields"},{"title":"module:MusicAlbumData.setPoster","link":"setPoster"},{"title":"module:MusicAlbumSongListData","link":"MusicAlbumSongListData"},{"title":"module:MusicAlbumSongListData.setFields","link":"setFields"},{"title":"module:MusicAlbumSongListData.setPoster","link":"setPoster"},{"title":"module:MusicArtistData","link":"MusicArtistData"},{"title":"module:MusicArtistData.setFields","link":"setFields"},{"title":"module:MusicArtistData.setPoster","link":"setPoster"},{"title":"module:MusicArtistGridItem","link":"MusicArtistGridItem"},{"title":"module:MusicArtistGridItem.focusChanged","link":"focusChanged","description":"

Display or hide title Visibility on focus change

"},{"title":"module:MusicArtistGridItem.init","link":"init"},{"title":"module:MusicArtistGridItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:MusicArtistGridItem.onPosterLoadStatusChanged","link":"onPosterLoadStatusChanged","description":"

Hide backdrop and text when poster loaded

"},{"title":"module:MusicLibraryView","link":"MusicLibraryView"},{"title":"module:MusicLibraryView.ItemDataLoaded","link":"ItemDataLoaded","description":"

Handle loaded data, and add to Grid

"},{"title":"module:MusicLibraryView.LogoImageLoaded","link":"LogoImageLoaded","description":"

Logo Image Loaded Event Handler

"},{"title":"module:MusicLibraryView.OnScreenHidden","link":"OnScreenHidden"},{"title":"module:MusicLibraryView.OnScreenShown","link":"OnScreenShown"},{"title":"module:MusicLibraryView.SetAlbumCount","link":"SetAlbumCount","description":"

Set Selected Artist Album Count

"},{"title":"module:MusicLibraryView.SetBackground","link":"SetBackground","description":"

Set Background Image

"},{"title":"module:MusicLibraryView.SetGenres","link":"SetGenres","description":"

Set Selected Artist Genres

"},{"title":"module:MusicLibraryView.SetName","link":"SetName","description":"

Set Selected Artist Name

"},{"title":"module:MusicLibraryView.SetSongCount","link":"SetSongCount","description":"

Set Selected Artist Song Count

"},{"title":"module:MusicLibraryView.SetUpOptions","link":"SetUpOptions","description":"

Data to display when options button selected

"},{"title":"module:MusicLibraryView.getCollectionType","link":"getCollectionType","description":"

Return parent collection type

"},{"title":"module:MusicLibraryView.getItemFocused","link":"getItemFocused","description":"

Returns Focused Item

"},{"title":"module:MusicLibraryView.inStringArray","link":"inStringArray","description":"

Search string array for search value. Return if it's found

"},{"title":"module:MusicLibraryView.init","link":"init"},{"title":"module:MusicLibraryView.loadInitialItems","link":"loadInitialItems","description":"

Load initial set of Data

"},{"title":"module:MusicLibraryView.loadMoreData","link":"loadMoreData","description":"

Load next set of items

"},{"title":"module:MusicLibraryView.newBGLoaded","link":"newBGLoaded","description":"

When Image Loading Status changes

"},{"title":"module:MusicLibraryView.onChannelSelected","link":"onChannelSelected"},{"title":"module:MusicLibraryView.onGenreItemFocused","link":"onGenreItemFocused","description":"

Genre Item Focused

"},{"title":"module:MusicLibraryView.onGenreItemSelected","link":"onGenreItemSelected","description":"

Genre Item Selected

"},{"title":"module:MusicLibraryView.onItemFocused","link":"onItemFocused","description":"

Handle new item being focused

"},{"title":"module:MusicLibraryView.onItemSelected","link":"onItemSelected","description":"

Item Selected

"},{"title":"module:MusicLibraryView.onItemalphaSelected","link":"onItemalphaSelected"},{"title":"module:MusicLibraryView.onKeyEvent","link":"onKeyEvent"},{"title":"module:MusicLibraryView.onvoiceFilter","link":"onvoiceFilter"},{"title":"module:MusicLibraryView.optionsClosed","link":"optionsClosed","description":"

Check if options updated and any reloading required

"},{"title":"module:MusicLibraryView.setFieldText","link":"setFieldText"},{"title":"module:MusicLibraryView.setMusicOptions","link":"setMusicOptions","description":"

Set Music view, sort, and filter options

"},{"title":"module:MusicLibraryView.setupNodes","link":"setupNodes"},{"title":"module:MusicLibraryView.swapDone","link":"swapDone","description":"

Swap Complete

"},{"title":"module:MusicSongData","link":"MusicSongData"},{"title":"module:MusicSongData.setFields","link":"setFields"},{"title":"module:MusicSongData.setPoster","link":"setPoster"},{"title":"module:OSD","link":"OSD"},{"title":"module:OSD.inactiveCheck","link":"inactiveCheck","description":"

inactiveCheck: Checks if the time since last keypress is greater than or equal to the allowed inactive time of the menu.

"},{"title":"module:OSD.init","link":"init"},{"title":"module:OSD.onButtonSelected","link":"onButtonSelected","description":"

onButtonSelected: Handler for selection of buttons from the menu.

"},{"title":"module:OSD.onFocusChanged","link":"onFocusChanged","description":"

onFocusChanged: Handler for changes to the focus of this menu.

"},{"title":"module:OSD.onItemTitleTextChanged","link":"onItemTitleTextChanged","description":"

onItemTitleTextChanged: Handler for changes to m.top.itemTitleText param.

"},{"title":"module:OSD.onKeyEvent","link":"onKeyEvent"},{"title":"module:OSD.onPlaybackStateChanged","link":"onPlaybackStateChanged","description":"

onPlaybackStateChanged: Handler for changes to m.top.playbackState param

"},{"title":"module:OSD.onProgressPercentageChanged","link":"onProgressPercentageChanged","description":"

onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param

"},{"title":"module:OSD.onVisibleChanged","link":"onVisibleChanged","description":"

onVisibleChanged: Handler for changes to the visibility of this menu.

"},{"title":"module:OSD.resetFocusToDefaultButton","link":"resetFocusToDefaultButton","description":"

resetFocusToDefaultButton: Reset focus back to the default button

"},{"title":"module:OptionNode","link":"OptionNode"},{"title":"module:OptionNode.init","link":"init"},{"title":"module:OptionsButton","link":"OptionsButton"},{"title":"module:OptionsButton.init","link":"init"},{"title":"module:OptionsButton.press","link":"press"},{"title":"module:OptionsData","link":"OptionsData"},{"title":"module:OptionsData.init","link":"init"},{"title":"module:OptionsData.press","link":"press"},{"title":"module:OptionsData.update_title","link":"update_title"},{"title":"module:OptionsSlider","link":"OptionsSlider"},{"title":"module:OptionsSlider.init","link":"init"},{"title":"module:OptionsSlider.onKeyEvent","link":"onKeyEvent"},{"title":"module:OptionsSlider.setFields","link":"setFields"},{"title":"module:OverviewDialog","link":"OverviewDialog"},{"title":"module:OverviewDialog.onKeyEvent","link":"onKeyEvent"},{"title":"module:OverviewDialog.setOverview","link":"setOverview"},{"title":"module:OverviewDialog.setTitle","link":"setTitle"},{"title":"module:PersonData","link":"PersonData"},{"title":"module:PersonData.setFields","link":"setFields"},{"title":"module:PersonData.setPoster","link":"setPoster"},{"title":"module:PersonDetails","link":"PersonDetails"},{"title":"module:PersonDetails.createDialogPallete","link":"createDialogPallete"},{"title":"module:PersonDetails.createFullDscrDlg","link":"createFullDscrDlg"},{"title":"module:PersonDetails.dscrShowFocus","link":"dscrShowFocus"},{"title":"module:PersonDetails.init","link":"init"},{"title":"module:PersonDetails.loadPerson","link":"loadPerson"},{"title":"module:PersonDetails.onButtonGroupEscaped","link":"onButtonGroupEscaped"},{"title":"module:PersonDetails.onKeyEvent","link":"onKeyEvent"},{"title":"module:PersonDetails.setFavoriteColor","link":"setFavoriteColor"},{"title":"module:PersonDetails.shortDate","link":"shortDate"},{"title":"module:PhotoData","link":"PhotoData"},{"title":"module:PhotoData.setFields","link":"setFields"},{"title":"module:PhotoData.setPoster","link":"setPoster"},{"title":"module:PhotoDetails","link":"PhotoDetails"},{"title":"module:PhotoDetails.OnScreenHidden","link":"OnScreenHidden","description":"

JFScreen hook.\nUsed to ensure tasks are stopped

"},{"title":"module:PhotoDetails.init","link":"init"},{"title":"module:PhotoDetails.isRandomChanged","link":"isRandomChanged","description":"

isRandom component field has changed

"},{"title":"module:PhotoDetails.isSlideshowChanged","link":"isSlideshowChanged","description":"

isSlideshow component field has changed

"},{"title":"module:PhotoDetails.isValidToContinue","link":"isValidToContinue"},{"title":"module:PhotoDetails.itemContentChanged","link":"itemContentChanged"},{"title":"module:PhotoDetails.nextSlide","link":"nextSlide"},{"title":"module:PhotoDetails.onKeyEvent","link":"onKeyEvent"},{"title":"module:PhotoDetails.onPhotoLoaded","link":"onPhotoLoaded"},{"title":"module:PhotoDetails.statusUpdate","link":"statusUpdate"},{"title":"module:PlaybackDialog","link":"PlaybackDialog"},{"title":"module:PlaybackDialog.onKeyEvent","link":"onKeyEvent"},{"title":"module:PlayedCheckmark","link":"PlayedCheckmark"},{"title":"module:PlayedCheckmark.init","link":"init"},{"title":"module:PlaylistData","link":"PlaylistData"},{"title":"module:PlaylistData.setFields","link":"setFields"},{"title":"module:PlaylistData.setPoster","link":"setPoster"},{"title":"module:PlaylistView","link":"PlaylistView"},{"title":"module:PlaylistView.adjustScreenForNoOverview","link":"adjustScreenForNoOverview","description":"

Adjust scene by removing overview node and showing more songs

"},{"title":"module:PlaylistView.createDialogPallete","link":"createDialogPallete"},{"title":"module:PlaylistView.createFullDscrDlg","link":"createFullDscrDlg"},{"title":"module:PlaylistView.init","link":"init"},{"title":"module:PlaylistView.onDoneLoading","link":"onDoneLoading"},{"title":"module:PlaylistView.onKeyEvent","link":"onKeyEvent"},{"title":"module:PlaylistView.pageContentChanged","link":"pageContentChanged","description":"

Set values for displayed values on screen

"},{"title":"module:PlaylistView.setOnScreenTextValues","link":"setOnScreenTextValues","description":"

Populate on screen text variables

"},{"title":"module:PlaylistView.setPosterImage","link":"setPosterImage","description":"

Set poster image on screen

"},{"title":"module:PlaylistView.setScreenTitle","link":"setScreenTitle","description":"

Set screen's title text

"},{"title":"module:PlaylistView.setupMainNode","link":"setupMainNode"},{"title":"module:PlaystateTask","link":"PlaystateTask"},{"title":"module:PlaystateTask.PlaystateDefaults","link":"PlaystateDefaults"},{"title":"module:PlaystateTask.PlaystateUpdate","link":"PlaystateUpdate"},{"title":"module:PlaystateTask.init","link":"init"},{"title":"module:PostTask","link":"PostTask"},{"title":"module:PostTask.asyncPost","link":"asyncPost","description":"

Post data and wait for response code

"},{"title":"module:PostTask.empty","link":"empty","description":"

Revert PostTask to default state

"},{"title":"module:PostTask.init","link":"init"},{"title":"module:PostTask.postItems","link":"postItems","description":"

Main function for PostTask.\nPosts either an array of data\nor a string of data to an API endpoint.\nSaves the response information

"},{"title":"module:ProgramDetails","link":"ProgramDetails"},{"title":"module:ProgramDetails.channelUpdated","link":"channelUpdated"},{"title":"module:ProgramDetails.focusChanged","link":"focusChanged","description":"

Show view channel button when item has Focus

"},{"title":"module:ProgramDetails.getDurationStringFromSeconds","link":"getDurationStringFromSeconds","description":"

Get program duration string (e.g. 1h 20m)

"},{"title":"module:ProgramDetails.getRelativeDayName","link":"getRelativeDayName","description":"

Get relative date name for a date (yesterday, today, tomorrow, or otherwise weekday name )

"},{"title":"module:ProgramDetails.init","link":"init"},{"title":"module:ProgramDetails.onAnimationComplete","link":"onAnimationComplete"},{"title":"module:ProgramDetails.onKeyEvent","link":"onKeyEvent"},{"title":"module:ProgramDetails.programUpdated","link":"programUpdated"},{"title":"module:ProgramDetails.setupLabels","link":"setupLabels","description":"

Set up Live and Repeat label sizes

"},{"title":"module:ProgramDetails.updateLabels","link":"updateLabels"},{"title":"module:PublicUserData","link":"PublicUserData"},{"title":"module:PublicUserData.init","link":"init"},{"title":"module:QueueManager","link":"QueueManager"},{"title":"module:QueueManager.clear","link":"clear","description":"

Clear all content from play queue

"},{"title":"module:QueueManager.clearHold","link":"clearHold","description":"

Clear all hold content

"},{"title":"module:QueueManager.deleteAtIndex","link":"deleteAtIndex","description":"

Delete item from play queue at passed index

"},{"title":"module:QueueManager.getCount","link":"getCount","description":"

Return the number of items in the play queue

"},{"title":"module:QueueManager.getCurrentItem","link":"getCurrentItem","description":"

Return the item currently in focus from the play queue

"},{"title":"module:QueueManager.getHold","link":"getHold","description":"

Return the items in the hold

"},{"title":"module:QueueManager.getIsShuffled","link":"getIsShuffled","description":"

Return whether or not shuffle is enabled

"},{"title":"module:QueueManager.getItemByIndex","link":"getItemByIndex","description":"

Return the item in the passed index from the play queue

"},{"title":"module:QueueManager.getItemType","link":"getItemType"},{"title":"module:QueueManager.getPosition","link":"getPosition","description":"

Returns current playback position within the queue

"},{"title":"module:QueueManager.getQueue","link":"getQueue","description":"

Return the current play queue

"},{"title":"module:QueueManager.getQueueTypes","link":"getQueueTypes","description":"

Return the types of items in current play queue

"},{"title":"module:QueueManager.getQueueUniqueTypes","link":"getQueueUniqueTypes","description":"

Return the unique types of items in current play queue

"},{"title":"module:QueueManager.getUnshuffledQueue","link":"getUnshuffledQueue","description":"

Return original, unshuffled queue

"},{"title":"module:QueueManager.hold","link":"hold","description":"

Hold an item

"},{"title":"module:QueueManager.init","link":"init"},{"title":"module:QueueManager.isPrerollActive","link":"isPrerollActive","description":"

Return isPrerollActive status

"},{"title":"module:QueueManager.moveBack","link":"moveBack","description":"

Move queue position back one

"},{"title":"module:QueueManager.moveForward","link":"moveForward","description":"

Move queue position ahead one

"},{"title":"module:QueueManager.peek","link":"peek","description":"

Return item at end of play queue without removing

"},{"title":"module:QueueManager.playQueue","link":"playQueue","description":"

Play items in queue

"},{"title":"module:QueueManager.pop","link":"pop","description":"

Remove item at end of play queue

"},{"title":"module:QueueManager.push","link":"push","description":"

Push new items to the play queue

"},{"title":"module:QueueManager.resetQueueItemOrder","link":"resetQueueItemOrder","description":"

Reset queue items back to original, unshuffled order

"},{"title":"module:QueueManager.resetShuffle","link":"resetShuffle","description":"

Reset shuffle to off state

"},{"title":"module:QueueManager.set","link":"set","description":"

Replace play queue with passed array

"},{"title":"module:QueueManager.setPosition","link":"setPosition","description":"

Set the queue position

"},{"title":"module:QueueManager.setPrerollStatus","link":"setPrerollStatus","description":"

Set prerollActive status

"},{"title":"module:QueueManager.setTopStartingPoint","link":"setTopStartingPoint","description":"

Set starting point for top item in the queue

"},{"title":"module:QueueManager.shuffleQueueItems","link":"shuffleQueueItems","description":"

Save a copy of the original queue and randomize order of queue items

"},{"title":"module:QueueManager.toggleShuffle","link":"toggleShuffle","description":"

Toggle shuffleEnabled state

"},{"title":"module:QueueManager.top","link":"top","description":"

Return the fitst item in the play queue

"},{"title":"module:QuickConnect","link":"QuickConnect"},{"title":"module:QuickConnect.init","link":"init"},{"title":"module:QuickConnect.monitorQuickConnect","link":"monitorQuickConnect"},{"title":"module:QuickConnectDialog","link":"QuickConnectDialog"},{"title":"module:QuickConnectDialog.OnAuthenticated","link":"OnAuthenticated"},{"title":"module:QuickConnectDialog.init","link":"init"},{"title":"module:QuickConnectDialog.onButtonSelected","link":"onButtonSelected"},{"title":"module:QuickConnectDialog.onKeyEvent","link":"onKeyEvent"},{"title":"module:QuickConnectDialog.quickConnectClosed","link":"quickConnectClosed"},{"title":"module:QuickConnectDialog.quickConnectStatus","link":"quickConnectStatus"},{"title":"module:RadioDialog","link":"RadioDialog"},{"title":"module:RadioDialog.init","link":"init"},{"title":"module:RadioDialog.moveScrollBar","link":"moveScrollBar","description":"

Move the popup's scroll bar

"},{"title":"module:RadioDialog.onButtonSelected","link":"onButtonSelected","description":"

Event handler for when user selected a button

"},{"title":"module:RadioDialog.onContentDataChanged","link":"onContentDataChanged"},{"title":"module:RadioDialog.onItemFocused","link":"onItemFocused","description":"

Event handler for when user's cursor highlights an option in the option list

"},{"title":"module:RadioDialog.onItemSelected","link":"onItemSelected","description":"

Once user selected an item, move cursor down to OK button

"},{"title":"module:RadioDialog.onKeyEvent","link":"onKeyEvent"},{"title":"module:RadioDialog.onScrollBarFocus","link":"onScrollBarFocus","description":"

If somehow the scrollbar gains focus, set focus back to the option list

"},{"title":"module:RecordProgramTask","link":"RecordProgramTask"},{"title":"module:RecordProgramTask.RecordOrCancelProgram","link":"RecordOrCancelProgram"},{"title":"module:RecordProgramTask.init","link":"init"},{"title":"module:SceneManager","link":"SceneManager"},{"title":"module:SceneManager.clearPreviousScene","link":"clearPreviousScene","description":"

Clear previous scene from group stack

"},{"title":"module:SceneManager.clearScenes","link":"clearScenes","description":"

Clear all content from group stack

"},{"title":"module:SceneManager.deleteSceneAtIndex","link":"deleteSceneAtIndex","description":"

Delete scene from group stack at passed index

"},{"title":"module:SceneManager.dismissDialog","link":"dismissDialog","description":"

Close currently displayed dialog

"},{"title":"module:SceneManager.getActiveScene","link":"getActiveScene","description":"

Return group at top of stack without removing

"},{"title":"module:SceneManager.init","link":"init"},{"title":"module:SceneManager.isDialogOpen","link":"isDialogOpen","description":"

Returns bool indicating if dialog is currently displayed

"},{"title":"module:SceneManager.optionClosed","link":"optionClosed","description":"

Return button the user selected

"},{"title":"module:SceneManager.optionDialog","link":"optionDialog","description":"

Display dialog to user with an OK button

"},{"title":"module:SceneManager.optionSelected","link":"optionSelected","description":"

Return button the user selected

"},{"title":"module:SceneManager.popScene","link":"popScene","description":"

Remove the current group and load the last group from the stack

"},{"title":"module:SceneManager.pushScene","link":"pushScene","description":"

Push a new group onto the stack, replacing the existing group on the screen

"},{"title":"module:SceneManager.radioDialog","link":"radioDialog","description":"

Display dialog to user with an OK button

"},{"title":"module:SceneManager.registerOverhangData","link":"registerOverhangData","description":"

Register observers for overhang data

"},{"title":"module:SceneManager.resetTime","link":"resetTime","description":"

Reset time

"},{"title":"module:SceneManager.settings","link":"settings","description":"

Display user/device settings screen

"},{"title":"module:SceneManager.standardDialog","link":"standardDialog","description":"

Display dialog to user with an OK button

"},{"title":"module:SceneManager.unregisterOverhangData","link":"unregisterOverhangData","description":"

Remove observers for overhang data

"},{"title":"module:SceneManager.updateOptions","link":"updateOptions","description":"

Update options availability

"},{"title":"module:SceneManager.updateOverhangTitle","link":"updateOverhangTitle","description":"

Update overhang title

"},{"title":"module:SceneManager.updateOverhangVisible","link":"updateOverhangVisible","description":"

Update whether the overhang is visible or not

"},{"title":"module:SceneManager.updateUser","link":"updateUser","description":"

Update username in overhang

"},{"title":"module:SceneManager.userMessage","link":"userMessage","description":"

Display dialog to user with an OK button

"},{"title":"module:ScheduleProgramData","link":"ScheduleProgramData"},{"title":"module:ScheduleProgramData.setFields","link":"setFields"},{"title":"module:ScheduleProgramData.setPoster","link":"setPoster"},{"title":"module:SearchBox","link":"SearchBox"},{"title":"module:SearchBox.init","link":"init"},{"title":"module:SearchBox.searchMedias","link":"searchMedias"},{"title":"module:SearchData","link":"SearchData"},{"title":"module:SearchData.setFields","link":"setFields"},{"title":"module:SearchData.setPoster","link":"setPoster"},{"title":"module:SearchResults","link":"SearchResults"},{"title":"module:SearchResults.init","link":"init"},{"title":"module:SearchResults.loadResults","link":"loadResults"},{"title":"module:SearchResults.onKeyEvent","link":"onKeyEvent"},{"title":"module:SearchResults.searchMedias","link":"searchMedias"},{"title":"module:SearchRow","link":"SearchRow"},{"title":"module:SearchRow.addRow","link":"addRow"},{"title":"module:SearchRow.getData","link":"getData"},{"title":"module:SearchRow.init","link":"init"},{"title":"module:SearchRow.updateSize","link":"updateSize"},{"title":"module:SearchTask","link":"SearchTask"},{"title":"module:SearchTask.init","link":"init"},{"title":"module:SearchTask.search","link":"search"},{"title":"module:SeriesData","link":"SeriesData"},{"title":"module:SeriesData.setFields","link":"setFields"},{"title":"module:SeriesData.setPoster","link":"setPoster"},{"title":"module:ServerDiscoveryTask","link":"ServerDiscoveryTask"},{"title":"module:ServerDiscoveryTask.AddServer","link":"AddServer"},{"title":"module:ServerDiscoveryTask.ProcessClientDiscoveryResponse","link":"ProcessClientDiscoveryResponse"},{"title":"module:ServerDiscoveryTask.ProcessSSDPResponse","link":"ProcessSSDPResponse"},{"title":"module:ServerDiscoveryTask.SendClientDiscoveryBroadcast","link":"SendClientDiscoveryBroadcast"},{"title":"module:ServerDiscoveryTask.SendSSDPBroadcast","link":"SendSSDPBroadcast"},{"title":"module:ServerDiscoveryTask.execute","link":"execute"},{"title":"module:ServerDiscoveryTask.init","link":"init","description":"

Task used to discover jellyfin servers on the local network

"},{"title":"module:SetServerScreen","link":"SetServerScreen"},{"title":"module:SetServerScreen.ScanForServers","link":"ScanForServers"},{"title":"module:SetServerScreen.ScanForServersComplete","link":"ScanForServersComplete"},{"title":"module:SetServerScreen.ShowKeyboard","link":"ShowKeyboard"},{"title":"module:SetServerScreen.clearErrorMessage","link":"clearErrorMessage"},{"title":"module:SetServerScreen.init","link":"init"},{"title":"module:SetServerScreen.onDialogButton","link":"onDialogButton"},{"title":"module:SetServerScreen.onKeyEvent","link":"onKeyEvent"},{"title":"module:ShowScenes","link":"ShowScenes"},{"title":"module:ShowScenes.CreateAlbumView","link":"CreateAlbumView","description":"

Shows details on selected album. Description text, image, and list of available songs

"},{"title":"module:ShowScenes.CreateArtistView","link":"CreateArtistView","description":"

Shows details on selected artist. Bio, image, and list of available albums

"},{"title":"module:ShowScenes.CreateHomeGroup","link":"CreateHomeGroup"},{"title":"module:ShowScenes.CreateItemGrid","link":"CreateItemGrid"},{"title":"module:ShowScenes.CreateMovieDetailsGroup","link":"CreateMovieDetailsGroup"},{"title":"module:ShowScenes.CreateMovieLibraryView","link":"CreateMovieLibraryView"},{"title":"module:ShowScenes.CreateMusicLibraryView","link":"CreateMusicLibraryView"},{"title":"module:ShowScenes.CreatePersonView","link":"CreatePersonView"},{"title":"module:ShowScenes.CreatePlaylistView","link":"CreatePlaylistView","description":"

Shows details on selected playlist. Description text, image, and list of available items

"},{"title":"module:ShowScenes.CreateSearchPage","link":"CreateSearchPage"},{"title":"module:ShowScenes.CreateSeasonDetailsGroup","link":"CreateSeasonDetailsGroup"},{"title":"module:ShowScenes.CreateSeasonDetailsGroupByID","link":"CreateSeasonDetailsGroupByID"},{"title":"module:ShowScenes.CreateSeriesDetailsGroup","link":"CreateSeriesDetailsGroup"},{"title":"module:ShowScenes.CreateServerGroup","link":"CreateServerGroup"},{"title":"module:ShowScenes.CreateSigninGroup","link":"CreateSigninGroup"},{"title":"module:ShowScenes.CreateUserSelectGroup","link":"CreateUserSelectGroup"},{"title":"module:ShowScenes.CreateVideoPlayerGroup","link":"CreateVideoPlayerGroup"},{"title":"module:ShowScenes.DeleteFromServerList","link":"DeleteFromServerList"},{"title":"module:ShowScenes.LoginFlow","link":"LoginFlow"},{"title":"module:ShowScenes.SaveServerList","link":"SaveServerList"},{"title":"module:ShowScenes.SendPerformanceBeacon","link":"SendPerformanceBeacon","description":"

Roku Performance monitoring

"},{"title":"module:ShowScenes.playbackOptionDialog","link":"playbackOptionDialog","description":"

Opens dialog asking user if they want to resume video or start playback over only on the home screen

"},{"title":"module:SlideOutButton","link":"SlideOutButton"},{"title":"module:SlideOutButton.init","link":"init"},{"title":"module:SlideOutButton.onBackgroundChanged","link":"onBackgroundChanged"},{"title":"module:SlideOutButton.onFocusChanged","link":"onFocusChanged"},{"title":"module:SlideOutButton.onHeightChanged","link":"onHeightChanged"},{"title":"module:SlideOutButton.onHighlightChanged","link":"onHighlightChanged"},{"title":"module:SlideOutButton.onIconChanged","link":"onIconChanged"},{"title":"module:SlideOutButton.onKeyEvent","link":"onKeyEvent"},{"title":"module:SlideOutButton.onPaddingChanged","link":"onPaddingChanged"},{"title":"module:SlideOutButton.onTextChanged","link":"onTextChanged"},{"title":"module:SlideOutButton.onWidthChanged","link":"onWidthChanged"},{"title":"module:SlideOutButton.setIconSize","link":"setIconSize"},{"title":"module:SongItem","link":"SongItem"},{"title":"module:SongItem.focusChanged","link":"focusChanged"},{"title":"module:SongItem.init","link":"init"},{"title":"module:SongItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:Spinner","link":"Spinner"},{"title":"module:Spinner.init","link":"init"},{"title":"module:StandardDialog","link":"StandardDialog"},{"title":"module:StandardDialog.init","link":"init"},{"title":"module:StandardDialog.onContentDataChanged","link":"onContentDataChanged"},{"title":"module:Subtitles","link":"Subtitles"},{"title":"module:Subtitles.availSubtitleTrackIdx","link":"availSubtitleTrackIdx","description":"

Roku translates the info provided in subtitleTracks into availableSubtitleTracks\nIncluding ignoring tracks, if they are not understood, thus making indexing unpredictable.\nThis function translates between our internel selected subtitle index\nand the corresponding index in availableSubtitleTracks.

"},{"title":"module:Subtitles.changeSubtitleDuringPlayback","link":"changeSubtitleDuringPlayback"},{"title":"module:Subtitles.defaultSubtitleTrack","link":"defaultSubtitleTrack","description":"

Identify the default subtitle track\nif "requires_text" is true, only return a track if it is textual\nThis allows forcing text subs, since roku requires transcoding of non-text subs\nreturns the server-side track index for the appriate subtitle

"},{"title":"module:Subtitles.defaultSubtitleTrackFromVid","link":"defaultSubtitleTrackFromVid","description":"

Identify the default subtitle track for a given video id\nreturns the server-side track index for the appriate subtitle

"},{"title":"module:Subtitles.getSubtitleLanguages","link":"getSubtitleLanguages"},{"title":"module:Subtitles.getSubtitleSelIdxFromSubIdx","link":"getSubtitleSelIdxFromSubIdx","description":"

The subtitle index on the server differs from the index we track locally\nThis function converts the former into the latter

"},{"title":"module:Subtitles.selectSubtitleTrack","link":"selectSubtitleTrack"},{"title":"module:Subtitles.selectSubtitleTrackDialog","link":"selectSubtitleTrackDialog","description":"

Present Dialog to user to select subtitle track

"},{"title":"module:Subtitles.setupSubtitle","link":"setupSubtitle","description":"

Given a set of subtitles, and a subtitle index (the index on the server, not in the list provided)\nthis will set all relevant settings for roku (mainly closed captions) and return the index of the\nsubtitle track specified, but indexed based on the provided list of subtitles

"},{"title":"module:Subtitles.sortSubtitles","link":"sortSubtitles","description":"

Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top

"},{"title":"module:Subtitles.turnoffSubtitles","link":"turnoffSubtitles"},{"title":"module:TVEpisode","link":"TVEpisode"},{"title":"module:TVEpisode.setFields","link":"setFields"},{"title":"module:TVEpisode.setPoster","link":"setPoster"},{"title":"module:TVEpisodeData","link":"TVEpisodeData"},{"title":"module:TVEpisodeData.setFields","link":"setFields"},{"title":"module:TVEpisodeData.setPoster","link":"setPoster"},{"title":"module:TVEpisodeRow","link":"TVEpisodeRow"},{"title":"module:TVEpisodeRow.init","link":"init"},{"title":"module:TVEpisodeRow.onKeyEvent","link":"onKeyEvent"},{"title":"module:TVEpisodeRow.setData","link":"setData"},{"title":"module:TVEpisodeRow.setupRows","link":"setupRows"},{"title":"module:TVEpisodeRow.updateSize","link":"updateSize"},{"title":"module:TVEpisodeRowWithOptions","link":"TVEpisodeRowWithOptions"},{"title":"module:TVEpisodeRowWithOptions.SetUpAudioOptions","link":"SetUpAudioOptions","description":"

List of audio tracks to choose from

"},{"title":"module:TVEpisodeRowWithOptions.SetUpVideoOptions","link":"SetUpVideoOptions","description":"

List of video versions to choose from

"},{"title":"module:TVEpisodeRowWithOptions.audioOptionsClosed","link":"audioOptionsClosed"},{"title":"module:TVEpisodeRowWithOptions.init","link":"init"},{"title":"module:TVEpisodeRowWithOptions.onKeyEvent","link":"onKeyEvent"},{"title":"module:TVEpisodeRowWithOptions.rowsDoneLoading","link":"rowsDoneLoading"},{"title":"module:TVEpisodeRowWithOptions.setupRows","link":"setupRows"},{"title":"module:TVEpisodeRowWithOptions.videoOptionsClosed","link":"videoOptionsClosed"},{"title":"module:TVEpisodes","link":"TVEpisodes"},{"title":"module:TVEpisodes.getFocusedItem","link":"getFocusedItem","description":"

get the currently focused item

"},{"title":"module:TVEpisodes.init","link":"init"},{"title":"module:TVEpisodes.onKeyEvent","link":"onKeyEvent","description":"

Handle navigation input from the remote and act on it

"},{"title":"module:TVEpisodes.setExtraButtonVisibility","link":"setExtraButtonVisibility","description":"

Updates the visibility of the Extras button based on if this season has any extra features

"},{"title":"module:TVEpisodes.setSeasonLoading","link":"setSeasonLoading"},{"title":"module:TVEpisodes.updateSeason","link":"updateSeason"},{"title":"module:TVListDetails","link":"TVListDetails"},{"title":"module:TVListDetails.DisplayAudioAvailable","link":"DisplayAudioAvailable","description":"

Adds "+N" (e.g. +1) if there is more than one audio track to choose from

"},{"title":"module:TVListDetails.DisplayVideoAvailable","link":"DisplayVideoAvailable","description":"

Adds "+N" (e.g. +1) if there is more than one video version to choose from

"},{"title":"module:TVListDetails.SetupAudioDisplay","link":"SetupAudioDisplay","description":"

Display current audio_codec and check if there is more than one audio track to choose from...

"},{"title":"module:TVListDetails.focusChanged","link":"focusChanged"},{"title":"module:TVListDetails.getEndTime","link":"getEndTime"},{"title":"module:TVListDetails.getRuntime","link":"getRuntime"},{"title":"module:TVListDetails.init","link":"init"},{"title":"module:TVListDetails.itemContentChanged","link":"itemContentChanged"},{"title":"module:TVListOptions","link":"TVListOptions"},{"title":"module:TVListOptions.buttonFocusChanged","link":"buttonFocusChanged","description":"

Switch menu shown when button focus changes

"},{"title":"module:TVListOptions.init","link":"init"},{"title":"module:TVListOptions.onKeyEvent","link":"onKeyEvent"},{"title":"module:TVListOptions.optionsSet","link":"optionsSet"},{"title":"module:TVSeasonData","link":"TVSeasonData"},{"title":"module:TVSeasonData.setFields","link":"setFields"},{"title":"module:TVSeasonData.setPoster","link":"setPoster"},{"title":"module:TVSeasonRow","link":"TVSeasonRow"},{"title":"module:TVSeasonRow.getData","link":"getData"},{"title":"module:TVSeasonRow.init","link":"init"},{"title":"module:TVSeasonRow.updateSize","link":"updateSize"},{"title":"module:TVShowDescription","link":"TVShowDescription"},{"title":"module:TVShowDescription.getEndTime","link":"getEndTime"},{"title":"module:TVShowDescription.getHistory","link":"getHistory"},{"title":"module:TVShowDescription.getRuntime","link":"getRuntime"},{"title":"module:TVShowDescription.init","link":"init"},{"title":"module:TVShowDescription.itemContentChanged","link":"itemContentChanged"},{"title":"module:TVShowDescription.round","link":"round"},{"title":"module:TVShowDescription.setFieldText","link":"setFieldText"},{"title":"module:TVShowDetails","link":"TVShowDetails"},{"title":"module:TVShowDetails.getEndTime","link":"getEndTime"},{"title":"module:TVShowDetails.getHistory","link":"getHistory"},{"title":"module:TVShowDetails.getRuntime","link":"getRuntime"},{"title":"module:TVShowDetails.init","link":"init"},{"title":"module:TVShowDetails.itemContentChanged","link":"itemContentChanged"},{"title":"module:TVShowDetails.onKeyEvent","link":"onKeyEvent"},{"title":"module:TVShowDetails.onShuffleEpisodeDataLoaded","link":"onShuffleEpisodeDataLoaded"},{"title":"module:TVShowDetails.round","link":"round"},{"title":"module:TVShowDetails.setFieldText","link":"setFieldText"},{"title":"module:TextSizeTask","link":"TextSizeTask"},{"title":"module:TextSizeTask.getTextSize","link":"getTextSize"},{"title":"module:TextSizeTask.init","link":"init"},{"title":"module:UserData","link":"UserData"},{"title":"module:UserData.getPreference","link":"getPreference"},{"title":"module:UserData.loadFromJSON","link":"loadFromJSON"},{"title":"module:UserData.loadFromRegistry","link":"loadFromRegistry"},{"title":"module:UserData.removeFromRegistry","link":"removeFromRegistry"},{"title":"module:UserData.saveToRegistry","link":"saveToRegistry"},{"title":"module:UserData.setDataFromJSON","link":"setDataFromJSON"},{"title":"module:UserData.setPreference","link":"setPreference"},{"title":"module:UserData.setServer","link":"setServer"},{"title":"module:UserItem","link":"UserItem"},{"title":"module:UserItem.init","link":"init"},{"title":"module:UserItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:UserLibrary","link":"UserLibrary"},{"title":"module:UserLibrary.MarkItemFavorite","link":"MarkItemFavorite"},{"title":"module:UserLibrary.MarkItemWatched","link":"MarkItemWatched"},{"title":"module:UserLibrary.UnmarkItemFavorite","link":"UnmarkItemFavorite"},{"title":"module:UserLibrary.UnmarkItemWatched","link":"UnmarkItemWatched"},{"title":"module:UserRow","link":"UserRow"},{"title":"module:UserRow.init","link":"init"},{"title":"module:UserRow.onKeyEvent","link":"onKeyEvent"},{"title":"module:UserRow.setData","link":"setData"},{"title":"module:UserRow.setUser","link":"setUser"},{"title":"module:UserRow.updateSize","link":"updateSize"},{"title":"module:UserSelect","link":"UserSelect"},{"title":"module:UserSelect.init","link":"init"},{"title":"module:UserSelect.itemContentChanged","link":"itemContentChanged"},{"title":"module:UserSelect.onKeyEvent","link":"onKeyEvent"},{"title":"module:UserSelect.redraw","link":"redraw"},{"title":"module:VideoData","link":"VideoData"},{"title":"module:VideoData.setFields","link":"setFields"},{"title":"module:VideoData.setPoster","link":"setPoster"},{"title":"module:VideoPlayer","link":"VideoPlayer"},{"title":"module:VideoPlayer.AddVideoContent","link":"AddVideoContent"},{"title":"module:VideoPlayer.GetPlaybackInfo","link":"GetPlaybackInfo","description":"

Returns an array of playback info to be displayed during playback.\nIn the future, with a custom playback info view, we can return an associated array.

"},{"title":"module:VideoPlayer.GetTranscodingStats","link":"GetTranscodingStats"},{"title":"module:VideoPlayer.PlayIntroVideo","link":"PlayIntroVideo"},{"title":"module:VideoPlayer.VideoPlayer","link":"VideoPlayer"},{"title":"module:VideoPlayer.autoPlayNextEpisode","link":"autoPlayNextEpisode"},{"title":"module:VideoPlayer.directPlaySupported","link":"directPlaySupported"},{"title":"module:VideoPlayer.getAudioFormat","link":"getAudioFormat"},{"title":"module:VideoPlayer.getAudioInfo","link":"getAudioInfo"},{"title":"module:VideoPlayer.getContainerType","link":"getContainerType"},{"title":"module:VideoPlayer.getDisplayBitrate","link":"getDisplayBitrate"},{"title":"module:VideoPlayer.getTranscodeReasons","link":"getTranscodeReasons","description":"

Extract array of Transcode Reasons from the content URL

"},{"title":"module:VideoPlayer.havePlaybackInfo","link":"havePlaybackInfo"},{"title":"module:VideoPlayer.startPlayBackOver","link":"startPlayBackOver","description":"

Opens dialog asking user if they want to resume video or start playback over only on the home screen

"},{"title":"module:VideoPlayerView","link":"VideoPlayerView"},{"title":"module:VideoPlayerView.ReportPlayback","link":"ReportPlayback","description":"

Report playback to server

"},{"title":"module:VideoPlayerView.bufferCheck","link":"bufferCheck","description":"

Check the the buffering has not hung

"},{"title":"module:VideoPlayerView.checkTimeToDisplayNextEpisode","link":"checkTimeToDisplayNextEpisode","description":"

Checks if we need to display the Next Episode button

"},{"title":"module:VideoPlayerView.getCurrentChapterIndex","link":"getCurrentChapterIndex","description":"

getCurrentChapterIndex: Finds current chapter index

"},{"title":"module:VideoPlayerView.handleChapterListAction","link":"handleChapterListAction","description":"

handleChapterListAction: Handles action to show chapter list

"},{"title":"module:VideoPlayerView.handleChapterSkipAction","link":"handleChapterSkipAction","description":"

handleChapterSkipAction: Handles user command to skip chapters in playing video

"},{"title":"module:VideoPlayerView.handleHideAction","link":"handleHideAction","description":"

handleHideAction: Handles action to hide OSD menu

"},{"title":"module:VideoPlayerView.handleShowSubtitleMenuAction","link":"handleShowSubtitleMenuAction","description":"

handleShowSubtitleMenuAction: Handles action to show subtitle selection menu

"},{"title":"module:VideoPlayerView.handleShowVideoInfoPopupAction","link":"handleShowVideoInfoPopupAction","description":"

handleShowVideoInfoPopupAction: Handles action to show video info popup

"},{"title":"module:VideoPlayerView.handleVideoPlayPauseAction","link":"handleVideoPlayPauseAction","description":"

handleVideoPlayPauseAction: Handles action to either play or pause the video content

"},{"title":"module:VideoPlayerView.hideNextEpisodeButton","link":"hideNextEpisodeButton","description":"

Runs hide Next Episode button animation and sets focus back to video

"},{"title":"module:VideoPlayerView.init","link":"init"},{"title":"module:VideoPlayerView.loadCaption","link":"loadCaption","description":"

Set caption url to server subtitle track

"},{"title":"module:VideoPlayerView.onAllowCaptionsChange","link":"onAllowCaptionsChange","description":"

Only setup caption items if captions are allowed

"},{"title":"module:VideoPlayerView.onContentChange","link":"onContentChange","description":"

Event handler for when video content field changes

"},{"title":"module:VideoPlayerView.onKeyEvent","link":"onKeyEvent"},{"title":"module:VideoPlayerView.onNextEpisodeDataLoaded","link":"onNextEpisodeDataLoaded"},{"title":"module:VideoPlayerView.onOSDAction","link":"onOSDAction","description":"

onOSDAction: Process action events from OSD to their respective handlers

"},{"title":"module:VideoPlayerView.onPlaybackErrorButtonSelected","link":"onPlaybackErrorButtonSelected"},{"title":"module:VideoPlayerView.onPlaybackErrorDialogClosed","link":"onPlaybackErrorDialogClosed"},{"title":"module:VideoPlayerView.onPositionChanged","link":"onPositionChanged","description":"

When Video Player state changes

"},{"title":"module:VideoPlayerView.onState","link":"onState","description":"

When Video Player state changes

"},{"title":"module:VideoPlayerView.onSubtitleChange","link":"onSubtitleChange","description":"

Event handler for when selectedSubtitle changes

"},{"title":"module:VideoPlayerView.onVideoContentLoaded","link":"onVideoContentLoaded"},{"title":"module:VideoPlayerView.populateChapterMenu","link":"populateChapterMenu","description":"

populateChapterMenu: ' Parse chapter data from API and appeand to chapter list menu

"},{"title":"module:VideoPlayerView.showNextEpisodeButton","link":"showNextEpisodeButton","description":"

Runs Next Episode button animation and sets focus to button

"},{"title":"module:VideoPlayerView.showPlaybackErrorDialog","link":"showPlaybackErrorDialog"},{"title":"module:VideoPlayerView.stateAllowsOSD","link":"stateAllowsOSD","description":"

stateAllowsOSD: Check if current video state allows showing the OSD

"},{"title":"module:VideoPlayerView.toggleCaption","link":"toggleCaption","description":"

Toggles visibility of custom subtitles and sets captionTask's player state

"},{"title":"module:VideoPlayerView.updateCaption","link":"updateCaption","description":"

Removes old subtitle lines and adds new subtitle lines

"},{"title":"module:VideoPlayerView.updateCount","link":"updateCount","description":"

Update count down text

"},{"title":"module:VideoTrackListItem","link":"VideoTrackListItem"},{"title":"module:VideoTrackListItem.focusChanged","link":"focusChanged","description":"

Scroll description if focused

"},{"title":"module:VideoTrackListItem.init","link":"init"},{"title":"module:VideoTrackListItem.itemContentChanged","link":"itemContentChanged"},{"title":"module:ViewCreator","link":"ViewCreator"},{"title":"module:ViewCreator.CreateAudioPlayerView","link":"CreateAudioPlayerView","description":"

Play Audio

"},{"title":"module:ViewCreator.CreateVideoPlayerView","link":"CreateVideoPlayerView","description":"

Play Video

"},{"title":"module:ViewCreator.availSubtitleTrackIdx","link":"availSubtitleTrackIdx","description":"

Roku translates the info provided in subtitleTracks into availableSubtitleTracks\nIncluding ignoring tracks, if they are not understood, thus making indexing unpredictable.\nThis function translates between our internel selected subtitle index\nand the corresponding index in availableSubtitleTracks.

"},{"title":"module:ViewCreator.onPlaybackInfoLoaded","link":"onPlaybackInfoLoaded","description":"

The playback info task has returned data

"},{"title":"module:ViewCreator.onSelectPlaybackInfoPressed","link":"onSelectPlaybackInfoPressed","description":"

User requested playback info

"},{"title":"module:ViewCreator.onSelectSubtitlePressed","link":"onSelectSubtitlePressed","description":"

User requested subtitle selection popup

"},{"title":"module:ViewCreator.onSelectionMade","link":"onSelectionMade","description":"

User has selected something from the radioDialog popup

"},{"title":"module:ViewCreator.onStateChange","link":"onStateChange","description":"

Playback state change event handlers

"},{"title":"module:ViewCreator.processSubtitleSelection","link":"processSubtitleSelection"},{"title":"module:WhatsNewDialog","link":"WhatsNewDialog"},{"title":"module:WhatsNewDialog.init","link":"init"},{"title":"module:WhatsNewDialog.setPalette","link":"setPalette"},{"title":"module:baserequest","link":"baserequest"},{"title":"module:baserequest.APIRequest","link":"APIRequest"},{"title":"module:baserequest.authRequest","link":"authRequest","description":"

Takes and returns a roUrlTransfer object after adding a Jellyfin "Authorization" header

"},{"title":"module:baserequest.buildAuthHeader","link":"buildAuthHeader","description":"

Returns a string containing the "Authorization" header payload

"},{"title":"module:baserequest.buildParams","link":"buildParams","description":"

Functions for making requests to the API

"},{"title":"module:baserequest.buildURL","link":"buildURL"},{"title":"module:baserequest.deleteVoid","link":"deleteVoid"},{"title":"module:baserequest.getJson","link":"getJson"},{"title":"module:baserequest.getString","link":"getString"},{"title":"module:baserequest.getVoid","link":"getVoid"},{"title":"module:baserequest.get_url","link":"get_url"},{"title":"module:baserequest.headVoid","link":"headVoid"},{"title":"module:baserequest.postJson","link":"postJson"},{"title":"module:baserequest.postString","link":"postString"},{"title":"module:baserequest.postVoid","link":"postVoid"},{"title":"module:baserequest.setCertificateAuthority","link":"setCertificateAuthority","description":"

sets the certificate authority by file path on the passed node

"},{"title":"module:captionTask","link":"captionTask"},{"title":"module:captionTask.fetchCaption","link":"fetchCaption"},{"title":"module:captionTask.init","link":"init"},{"title":"module:captionTask.isTime","link":"isTime"},{"title":"module:captionTask.newLayoutGroup","link":"newLayoutGroup"},{"title":"module:captionTask.newRect","link":"newRect"},{"title":"module:captionTask.newlabel","link":"newlabel"},{"title":"module:captionTask.parseVTT","link":"parseVTT"},{"title":"module:captionTask.setFont","link":"setFont"},{"title":"module:captionTask.toMs","link":"toMs"},{"title":"module:captionTask.updateCaption","link":"updateCaption"},{"title":"module:conditional","link":"conditional"},{"title":"module:conditional.printRegistry","link":"printRegistry","description":"

Print out all of the registry contents to the debug log

"},{"title":"module:config","link":"config"},{"title":"module:config.GetConfigTree","link":"GetConfigTree","description":"

Read config tree from json config file and return

"},{"title":"module:config.RegistryReadAll","link":"RegistryReadAll","description":"

Return all data found inside a registry section

"},{"title":"module:config.findConfigTreeKey","link":"findConfigTreeKey","description":"

Recursivly search the config tree for entry with settingname equal to key

"},{"title":"module:config.getRegistrySections","link":"getRegistrySections","description":"

Return an array of all the registry section keys

"},{"title":"module:config.getSavedUsers","link":"getSavedUsers","description":"

Returns an array of saved users from the registry\nthat belong to the active server

"},{"title":"module:config.get_setting","link":"get_setting","description":"

"Jellyfin" registry accessors for the default global settings

"},{"title":"module:config.get_user_setting","link":"get_user_setting","description":"

User registry accessors for the currently active user

"},{"title":"module:config.registry_delete","link":"registry_delete"},{"title":"module:config.registry_read","link":"registry_read","description":"

Generic registry accessors

"},{"title":"module:config.registry_write","link":"registry_write"},{"title":"module:config.set_setting","link":"set_setting"},{"title":"module:config.set_user_setting","link":"set_user_setting"},{"title":"module:config.unset_setting","link":"unset_setting"},{"title":"module:config.unset_user_setting","link":"unset_user_setting"},{"title":"module:deviceCapabilities","link":"deviceCapabilities"},{"title":"module:deviceCapabilities.GetBitRateLimit","link":"GetBitRateLimit"},{"title":"module:deviceCapabilities.GetDirectPlayProfiles","link":"GetDirectPlayProfiles"},{"title":"module:deviceCapabilities.getCodecProfiles","link":"getCodecProfiles"},{"title":"module:deviceCapabilities.getContainerProfiles","link":"getContainerProfiles"},{"title":"module:deviceCapabilities.getDeviceCapabilities","link":"getDeviceCapabilities","description":"

Returns the Device Capabilities for Roku.\nAlso prints out the device profile for debugging

"},{"title":"module:deviceCapabilities.getDeviceProfile","link":"getDeviceProfile"},{"title":"module:deviceCapabilities.getMaxHeightArray","link":"getMaxHeightArray"},{"title":"module:deviceCapabilities.getMaxWidthArray","link":"getMaxWidthArray"},{"title":"module:deviceCapabilities.getSubtitleProfiles","link":"getSubtitleProfiles"},{"title":"module:deviceCapabilities.getTranscodingProfiles","link":"getTranscodingProfiles"},{"title":"module:deviceCapabilities.printDeviceProfile","link":"printDeviceProfile","description":"

Print out the deviceProfile for debugging

"},{"title":"module:deviceCapabilities.removeDecimals","link":"removeDecimals","description":"

Remove all decimals from a string

"},{"title":"module:deviceCapabilities.setPreferredCodec","link":"setPreferredCodec","description":"

Takes and returns a comma delimited string of codecs.\nMoves the preferred codec to the front of the string

"},{"title":"module:deviceCapabilities.updateProfileArray","link":"updateProfileArray","description":"

Recieves and returns an assArray of supported profiles and levels for each video codec

"},{"title":"module:globals","link":"globals"},{"title":"module:globals.SaveAppToGlobal","link":"SaveAppToGlobal","description":"

Save information from roAppInfo to m.global.app

"},{"title":"module:globals.SaveDeviceToGlobal","link":"SaveDeviceToGlobal","description":"

Save information from roDeviceInfo to m.global.device

"},{"title":"module:globals.setConstants","link":"setConstants","description":"

Set global constants

"},{"title":"module:homeRowItemSizes","link":"homeRowItemSizes"},{"title":"module:migrations","link":"migrations"},{"title":"module:migrations.runGlobalMigrations","link":"runGlobalMigrations","description":"

Run all necessary registry mirations on the "global" Jellyfin registry section

"},{"title":"module:migrations.runRegistryUserMigrations","link":"runRegistryUserMigrations"},{"title":"module:misc","link":"misc"},{"title":"module:misc.AssocArrayEqual","link":"AssocArrayEqual"},{"title":"module:misc.arrayHasValue","link":"arrayHasValue","description":"

Check if a specific value is inside of an array

"},{"title":"module:misc.div_ceiling","link":"div_ceiling"},{"title":"module:misc.findNodeBySubtype","link":"findNodeBySubtype"},{"title":"module:misc.formatTime","link":"formatTime","description":"

Format time as 12 or 24 hour format based on system clock setting

"},{"title":"module:misc.getButton","link":"getButton"},{"title":"module:misc.getMinutes","link":"getMinutes","description":"

Converts ticks to minutes

"},{"title":"module:misc.getMsgPicker","link":"getMsgPicker"},{"title":"module:misc.get_dialog_result","link":"get_dialog_result","description":"

Returns the item selected or -1 on backpress or other unhandled closure of dialog.

"},{"title":"module:misc.inArray","link":"inArray","description":"

Search string array for search value. Return if it's found

"},{"title":"module:misc.inferServerUrl","link":"inferServerUrl","description":"

take an incomplete url string and use it to make educated guesses about\nthe complete url. then tests these guesses to see if it can find a jf server\nreturns the url of the server it found, or an empty string

"},{"title":"module:misc.isJellyfinServer","link":"isJellyfinServer","description":"

accepts the raw json string of /system/info/public and returns\na boolean indicating if ProductName is "Jellyfin Server"

"},{"title":"module:misc.isLocalhost","link":"isLocalhost","description":"

Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'

"},{"title":"module:misc.isNodeEvent","link":"isNodeEvent"},{"title":"module:misc.isValid","link":"isValid","description":"

Returns whether or not passed value is valid

"},{"title":"module:misc.isValidAndNotEmpty","link":"isValidAndNotEmpty","description":"

Returns whether or not passed value is valid and not empty\nAccepts a string, or any countable type (arrays and lists)

"},{"title":"module:misc.lastFocusedChild","link":"lastFocusedChild"},{"title":"module:misc.leftPad","link":"leftPad"},{"title":"module:misc.message_dialog","link":"message_dialog"},{"title":"module:misc.option_dialog","link":"option_dialog"},{"title":"module:misc.parseUrl","link":"parseUrl","description":"

Returns an array from a url = [ url, proto, host, port, subdir+params ]\nIf port or subdir are not found, an empty string will be added to the array\nProto must be declared or array will be empty

"},{"title":"module:misc.roundNumber","link":"roundNumber","description":"

Rounds number to nearest integer

"},{"title":"module:misc.secondsToHuman","link":"secondsToHuman"},{"title":"module:misc.setFieldTextValue","link":"setFieldTextValue"},{"title":"module:misc.show_dialog","link":"show_dialog"},{"title":"module:misc.shuffleArray","link":"shuffleArray","description":"

Takes an array of data, shuffles the order, then returns the array\nuses the Fisher-Yates shuffling algorithm

"},{"title":"module:misc.startLoadingSpinner","link":"startLoadingSpinner","description":"

startLoadingSpinner: Start a loading spinner and attach it to the main JFScene.\nDisplays an invisible ProgressDialog node by default to disable keypresses while loading.

"},{"title":"module:misc.stopLoadingSpinner","link":"stopLoadingSpinner"},{"title":"module:misc.ticksToHuman","link":"ticksToHuman"},{"title":"module:misc.toString","link":"toString"},{"title":"module:misc.urlCandidates","link":"urlCandidates","description":"

this is the "educated guess" logic for inferServerUrl that generates a list of complete url's as candidates\nfor the tests in inferServerUrl. takes an incomplete url as an arg and returns a list of extrapolated\nfull urls.

"},{"title":"module:misc.versionChecker","link":"versionChecker","description":"

Returns whether or not a version number (e.g. 10.7.7) is greater or equal\nto some minimum version allowed (e.g. 10.8.0)

"},{"title":"module:quickplay","link":"quickplay"},{"title":"module:quickplay.album","link":"album","description":"

A music album.\nPlay the entire album starting with track 1.

"},{"title":"module:quickplay.artist","link":"artist","description":"

A music artist.\nShuffle play all songs by artist.

"},{"title":"module:quickplay.audio","link":"audio","description":"

A single audio file.

"},{"title":"module:quickplay.boxset","link":"boxset","description":"

A boxset.\nPlay all items inside.

"},{"title":"module:quickplay.collectionFolder","link":"collectionFolder","description":"

Quick Play A CollectionFolder.\nShuffle play the items inside\nwith some differences based on collectionType.

"},{"title":"module:quickplay.folder","link":"folder","description":"

Quick Play A folder.\nShuffle play all items found

"},{"title":"module:quickplay.multipleSeries","link":"multipleSeries","description":"

More than one TV Show Series.\nShuffle play all watched episodes

"},{"title":"module:quickplay.musicVideo","link":"musicVideo","description":"

A single music video file.

"},{"title":"module:quickplay.person","link":"person","description":"

Quick Play A Person.\nShuffle play all videos found

"},{"title":"module:quickplay.photo","link":"photo","description":"

A single photo.

"},{"title":"module:quickplay.photoAlbum","link":"photoAlbum","description":"

A photo album.

"},{"title":"module:quickplay.playlist","link":"playlist","description":"

Quick Play A Playlist.\nPlay the first unwatched episode.\nIf none, play the whole season starting with episode 1.

"},{"title":"module:quickplay.program","link":"program","description":"

Quick Play A Live Program

"},{"title":"module:quickplay.pushToQueue","link":"pushToQueue","description":"

Takes an array of items and adds to global queue.\nAlso shuffles the playlist if asked

"},{"title":"module:quickplay.season","link":"season","description":"

A TV Show Season.\nPlay the first unwatched episode.\nIf none, play the whole season starting with episode 1.

"},{"title":"module:quickplay.series","link":"series","description":"

A TV Show Series.\nPlay the first unwatched episode.\nIf none, shuffle play the whole series.

"},{"title":"module:quickplay.tvChannel","link":"tvChannel","description":"

Quick Play A TVChannel

"},{"title":"module:quickplay.userView","link":"userView","description":"

Quick Play A UserView.\nPlay logic depends on "collectionType".

"},{"title":"module:quickplay.video","link":"video","description":"

A single video file.

"},{"title":"module:quickplay.videoContainer","link":"videoContainer","description":"

A container with some kind of videos inside of it

"},{"title":"module:schedule","link":"schedule"},{"title":"module:schedule.channelFilterSet","link":"channelFilterSet"},{"title":"module:schedule.channelsearchTermSet","link":"channelsearchTermSet","description":"

Voice Search set

"},{"title":"module:schedule.focusProgramDetails","link":"focusProgramDetails","description":"

Move the TV Guide Grid down or up depending whether details are selected

"},{"title":"module:schedule.init","link":"init"},{"title":"module:schedule.onChannelsLoaded","link":"onChannelsLoaded","description":"

Initial list of channels loaded

"},{"title":"module:schedule.onGridScrolled","link":"onGridScrolled","description":"

As user scrolls grid, check if more data requries to be loaded

"},{"title":"module:schedule.onKeyEvent","link":"onKeyEvent"},{"title":"module:schedule.onProgramDetailsLoaded","link":"onProgramDetailsLoaded","description":"

Update the Program Details with full information

"},{"title":"module:schedule.onProgramFocused","link":"onProgramFocused"},{"title":"module:schedule.onProgramSelected","link":"onProgramSelected"},{"title":"module:schedule.onRecordChannelSelected","link":"onRecordChannelSelected","description":"

Handle user selecting "Record Channel" from Program Details

"},{"title":"module:schedule.onRecordOperationDone","link":"onRecordOperationDone"},{"title":"module:schedule.onRecordSeriesChannelSelected","link":"onRecordSeriesChannelSelected","description":"

Handle user selecting "Record Series" from Program Details

"},{"title":"module:schedule.onScheduleLoaded","link":"onScheduleLoaded","description":"

When LoadScheduleTask completes (initial or more data) and we have a schedule to display

"},{"title":"module:schedule.onWatchChannelSelected","link":"onWatchChannelSelected","description":"

Handle user selecting "Watch Channel" from Program Details

"},{"title":"module:section","link":"section"},{"title":"module:section.init","link":"init"},{"title":"module:section.onFocusChange","link":"onFocusChange"},{"title":"module:section.onIDChange","link":"onIDChange"},{"title":"module:section.onTranslationChange","link":"onTranslationChange"},{"title":"module:section.scrollDownToOnDeck","link":"scrollDownToOnDeck"},{"title":"module:section.scrollOffBottom","link":"scrollOffBottom"},{"title":"module:section.scrollOffOnDeck","link":"scrollOffOnDeck"},{"title":"module:section.scrollOffTop","link":"scrollOffTop"},{"title":"module:section.scrollUpToOnDeck","link":"scrollUpToOnDeck"},{"title":"module:section.showFromBottom","link":"showFromBottom"},{"title":"module:section.showFromTop","link":"showFromTop"},{"title":"module:sectionScroller","link":"sectionScroller"},{"title":"module:sectionScroller.displayedIndexChanged","link":"displayedIndexChanged"},{"title":"module:sectionScroller.init","link":"init"},{"title":"module:sectionScroller.onFocusChange","link":"onFocusChange"},{"title":"module:settings","link":"settings"},{"title":"module:settings.LoadMenu","link":"LoadMenu"},{"title":"module:settings.OnScreenHidden","link":"OnScreenHidden","description":"

JFScreen hook that gets ran as needed.\nAssumes settings were changed and they affect the device profile.\nPosts a new device profile to the server using the task thread

"},{"title":"module:settings.boolSettingChanged","link":"boolSettingChanged"},{"title":"module:settings.init","link":"init"},{"title":"module:settings.isFormInFocus","link":"isFormInFocus","description":"

Returns true if any of the data entry forms are in focus

"},{"title":"module:settings.onKeyEvent","link":"onKeyEvent"},{"title":"module:settings.onKeyGridEscape","link":"onKeyGridEscape"},{"title":"module:settings.onKeyGridSubmit","link":"onKeyGridSubmit"},{"title":"module:settings.postFinished","link":"postFinished","description":"

Triggered by m.postTask after completing a post.\nEmpty the task data when finished.

"},{"title":"module:settings.radioSettingChanged","link":"radioSettingChanged"},{"title":"module:settings.settingFocused","link":"settingFocused"},{"title":"module:settings.settingSelected","link":"settingSelected"},{"title":"module:userauth","link":"userauth"},{"title":"module:userauth.AboutMe","link":"AboutMe"},{"title":"module:userauth.AuthenticateViaQuickConnect","link":"AuthenticateViaQuickConnect"},{"title":"module:userauth.AvailableUsers","link":"AvailableUsers"},{"title":"module:userauth.GetPublicUsers","link":"GetPublicUsers"},{"title":"module:userauth.LoadUserAbilities","link":"LoadUserAbilities"},{"title":"module:userauth.ServerInfo","link":"ServerInfo"},{"title":"module:userauth.SignOut","link":"SignOut"},{"title":"module:userauth.checkQuickConnect","link":"checkQuickConnect"},{"title":"module:userauth.get_token","link":"get_token"},{"title":"module:userauth.initQuickConnect","link":"initQuickConnect"}]} \ No newline at end of file diff --git a/docs/api/fonts/Inconsolata-Regular.ttf b/docs/api/fonts/Inconsolata-Regular.ttf new file mode 100644 index 000000000..457d262cf Binary files /dev/null and b/docs/api/fonts/Inconsolata-Regular.ttf differ diff --git a/docs/api/fonts/OpenSans-Regular.ttf b/docs/api/fonts/OpenSans-Regular.ttf new file mode 100644 index 000000000..e21ff5f1e Binary files /dev/null and b/docs/api/fonts/OpenSans-Regular.ttf differ diff --git a/docs/api/fonts/WorkSans-Bold.ttf b/docs/api/fonts/WorkSans-Bold.ttf new file mode 100644 index 000000000..0caaf4d4d Binary files /dev/null and b/docs/api/fonts/WorkSans-Bold.ttf differ diff --git a/docs/api/index.html b/docs/api/index.html new file mode 100644 index 000000000..e8f2e26b9 --- /dev/null +++ b/docs/api/index.html @@ -0,0 +1,3 @@ +jellyfin-roku Code Documentation
On this page

Welcome

Use the Modules dropdown or the search feature to find functions and files to inspect

Known Issues

  • BrighterScript namespaces:
    • Duplicate function names will prevent the entire file from being parsed by JSDoc i.e. having namespace.red.Delete() and namespace.blue.Delete()
  • The line numbers listed for functions are incorrect.
  • The syntax highlighter treats all source files as JavaScript.
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AlbumData.html b/docs/api/module-AlbumData.html new file mode 100644 index 000000000..762053322 --- /dev/null +++ b/docs/api/module-AlbumData.html @@ -0,0 +1,3 @@ +Module: AlbumData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AlbumGrid.html b/docs/api/module-AlbumGrid.html new file mode 100644 index 000000000..14b96c4a9 --- /dev/null +++ b/docs/api/module-AlbumGrid.html @@ -0,0 +1,3 @@ +Module: AlbumGrid
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AlbumTrackList.html b/docs/api/module-AlbumTrackList.html new file mode 100644 index 000000000..37faaaaca --- /dev/null +++ b/docs/api/module-AlbumTrackList.html @@ -0,0 +1,3 @@ +Module: AlbumTrackList
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AlbumView.html b/docs/api/module-AlbumView.html new file mode 100644 index 000000000..2b8d6eba4 --- /dev/null +++ b/docs/api/module-AlbumView.html @@ -0,0 +1,3 @@ +Module: AlbumView
On this page

Methods

(static) adjustScreenForNoOverview() → {void}

Adjust scene by removing overview node and showing more songs

Returns:
Type: 
void

(static) createDialogPallete() → {void}

Returns:
Type: 
void

(static) createFullDscrDlg() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onDoneLoading() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) pageContentChanged() → {void}

Set values for displayed values on screen

Returns:
Type: 
void

(static) setOnScreenTextValues(json) → {void}

Populate on screen text variables

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setPosterImage(posterURL) → {void}

Set poster image on screen

Parameters:
NameTypeDescription
posterURLdynamic
Returns:
Type: 
void

(static) setScreenTitle(json) → {void}

Set screen's title text

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setupMainNode() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Alpha.html b/docs/api/module-Alpha.html new file mode 100644 index 000000000..280448a5b --- /dev/null +++ b/docs/api/module-Alpha.html @@ -0,0 +1,3 @@ +Module: Alpha
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ArtistView.html b/docs/api/module-ArtistView.html new file mode 100644 index 000000000..78bb68ce6 --- /dev/null +++ b/docs/api/module-ArtistView.html @@ -0,0 +1,3 @@ +Module: ArtistView
On this page

Methods

(static) OnScreenHidden() → {void}

Returns:
Type: 
void

(static) OnScreenShown() → {void}

Returns:
Type: 
void

(static) artistOverviewChanged() → {void}

Event fired when page data is loaded

Returns:
Type: 
void

(static) createDialogPallete() → {void}

Returns:
Type: 
void

(static) createFullDscrDlg() → {void}

Returns:
Type: 
void

(static) dscrShowFocus() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onAlbumsData() → {void}

Returns:
Type: 
void

(static) onAlbumsEscape() → {void}

Returns:
Type: 
void

(static) onAppearsOnData() → {void}

Returns:
Type: 
void

(static) onAppearsOnEscape() → {void}

Returns:
Type: 
void

(static) onBackdropImageLoaded() → {void}

Returns:
Type: 
void

(static) onButtonSelectedChange() → {void}

Event handler when user selected a different playback button

Returns:
Type: 
void

(static) onEllipsisChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onSectionNavigationEscape() → {void}

Returns:
Type: 
void

(static) onSectionNavigationSelected() → {void}

Returns:
Type: 
void

(static) onSectionScrollerChange() → {void}

Returns:
Type: 
void

(static) pageContentChanged() → {void}

Event fired when page data is loaded

Returns:
Type: 
void

(static) setBackdropImage(data) → {void}

Add backdrop image to screen

Parameters:
NameTypeDescription
datadynamic
Returns:
Type: 
void

(static) setPosterImage(posterURL) → {void}

Parameters:
NameTypeDescription
posterURLdynamic
Returns:
Type: 
void

(static) setScreenTitle(json) → {void}

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setupButtons() → {void}

Setup playback buttons, default to Play button selected

Returns:
Type: 
void

(static) setupMainNode() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AudioPlayer.html b/docs/api/module-AudioPlayer.html new file mode 100644 index 000000000..fa318ee73 --- /dev/null +++ b/docs/api/module-AudioPlayer.html @@ -0,0 +1,3 @@ +Module: AudioPlayer
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AudioPlayerView.html b/docs/api/module-AudioPlayerView.html new file mode 100644 index 000000000..18ea01a98 --- /dev/null +++ b/docs/api/module-AudioPlayerView.html @@ -0,0 +1,3 @@ +Module: AudioPlayerView
On this page

Methods

(static) LoadNextSong() → {void}

Returns:
Type: 
void

(static) OnScreenHidden() → {void}

Returns:
Type: 
void

(static) audioPositionChanged() → {void}

Returns:
Type: 
void

(static) audioStateChanged() → {void}

Returns:
Type: 
void

(static) bufferPositionChanged() → {void}

Returns:
Type: 
void

(static) endScreenSaver() → {void}

Returns:
Type: 
void

(static) findCurrentSongIndex(songList) → {integer}

Parameters:
NameTypeDescription
songListdynamic
Returns:
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) loadButtons() → {void}

If we have more and 1 song to play, fade in the next and previous controls

Returns:
Type: 
void

(static) loopClicked() → {boolean}

Returns:
Type: 
boolean

(static) nextClicked() → {boolean}

Returns:
Type: 
boolean

(static) onAudioStreamLoaded() → {void}

Returns:
Type: 
void

(static) onBackdropImageLoaded() → {void}

Returns:
Type: 
void

(static) onButtonSelectedChange() → {void}

Event handler when user selected a different playback button

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Process key press events

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onMetaDataLoaded() → {void}

Returns:
Type: 
void

(static) onScreensaverTimeoutLoaded() → {void}

Returns:
Type: 
void

(static) pageContentChanged() → {void}

Update values on screen when page content changes

Returns:
Type: 
void

(static) playAction() → {boolean}

Returns:
Type: 
boolean

(static) previousClicked() → {boolean}

Returns:
Type: 
boolean

(static) resetLoopModeToDefault() → {void}

Returns:
Type: 
void

(static) screenSaverActive() → {boolean}

Returns:
Type: 
boolean

(static) setBackdropImage(data) → {void}

Add backdrop image to screen

Parameters:
NameTypeDescription
datadynamic
Returns:
Type: 
void

(static) setLoopButtonImage() → {void}

Returns:
Type: 
void

(static) setOnScreenTextValues(json) → {void}

Populate on screen text variables

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setPosterImage(posterURL) → {void}

Set poster image on screen

Parameters:
NameTypeDescription
posterURLdynamic
Returns:
Type: 
void

(static) setScreenTitle(json) → {void}

Set screen's title text

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setShuffleIconState() → {void}

Returns:
Type: 
void

(static) setTrackNumberDisplay() → {void}

Returns:
Type: 
void

(static) setupAnimationTasks() → {void}

Returns:
Type: 
void

(static) setupAudioNode() → {void}

Creates audio node used to play song(s)

Returns:
Type: 
void

(static) setupButtons() → {void}

Setup playback buttons, default to Play button selected

Returns:
Type: 
void

(static) setupDataTasks() → {void}

Creates tasks to gather data needed to render Scene and play song

Returns:
Type: 
void

(static) setupInfoNodes() → {void}

Returns:
Type: 
void

(static) setupScreenSaver() → {void}

Returns:
Type: 
void

(static) shuffleClicked() → {boolean}

Returns:
Type: 
boolean

(static) startScreenSaver() → {void}

Returns:
Type: 
void

(static) toggleShuffleEnabled() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-AudioTrackListItem.html b/docs/api/module-AudioTrackListItem.html new file mode 100644 index 000000000..18323f9f5 --- /dev/null +++ b/docs/api/module-AudioTrackListItem.html @@ -0,0 +1,3 @@ +Module: AudioTrackListItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ButtonGroupHoriz.html b/docs/api/module-ButtonGroupHoriz.html new file mode 100644 index 000000000..46ab223a7 --- /dev/null +++ b/docs/api/module-ButtonGroupHoriz.html @@ -0,0 +1,3 @@ +Module: ButtonGroupHoriz
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ButtonGroupVert.html b/docs/api/module-ButtonGroupVert.html new file mode 100644 index 000000000..37b4969b1 --- /dev/null +++ b/docs/api/module-ButtonGroupVert.html @@ -0,0 +1,3 @@ +Module: ButtonGroupVert
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onFocusButtonChanged() → {void}

Returns:
Type: 
void

(static) onFocusChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ChannelData.html b/docs/api/module-ChannelData.html new file mode 100644 index 000000000..b2bad7436 --- /dev/null +++ b/docs/api/module-ChannelData.html @@ -0,0 +1,3 @@ +Module: ChannelData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Clock.html b/docs/api/module-Clock.html new file mode 100644 index 000000000..94e017157 --- /dev/null +++ b/docs/api/module-Clock.html @@ -0,0 +1,3 @@ +Module: Clock
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-CollectionData.html b/docs/api/module-CollectionData.html new file mode 100644 index 000000000..bc5dbb4f6 --- /dev/null +++ b/docs/api/module-CollectionData.html @@ -0,0 +1,3 @@ +Module: CollectionData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ConfigData.html b/docs/api/module-ConfigData.html new file mode 100644 index 000000000..5594b5056 --- /dev/null +++ b/docs/api/module-ConfigData.html @@ -0,0 +1,3 @@ +Module: ConfigData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ConfigItem.html b/docs/api/module-ConfigItem.html new file mode 100644 index 000000000..c51b56ae3 --- /dev/null +++ b/docs/api/module-ConfigItem.html @@ -0,0 +1,3 @@ +Module: ConfigItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ConfigList.html b/docs/api/module-ConfigList.html new file mode 100644 index 000000000..aabeb2277 --- /dev/null +++ b/docs/api/module-ConfigList.html @@ -0,0 +1,3 @@ +Module: ConfigList
On this page

Methods

(static) configListShowDialog(configField) → {void}

Parameters:
NameTypeDescription
configFielddynamic
Returns:
Type: 
void

(static) dismiss_dialog() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onDialogButton() → {dynamic}

Returns:
Type: 
dynamic

(static) onItemSelected() → {void}

Returns:
Type: 
void

(static) setData() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ExtrasItem.html b/docs/api/module-ExtrasItem.html new file mode 100644 index 000000000..414a61bb8 --- /dev/null +++ b/docs/api/module-ExtrasItem.html @@ -0,0 +1,3 @@ +Module: ExtrasItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ExtrasRowList.html b/docs/api/module-ExtrasRowList.html new file mode 100644 index 000000000..84f6b9d37 --- /dev/null +++ b/docs/api/module-ExtrasRowList.html @@ -0,0 +1,3 @@ +Module: ExtrasRowList
On this page

Methods

(static) addRowSize(newRow) → {void}

Parameters:
NameTypeDescription
newRowdynamic
Returns:
Type: 
void

(static) buildRow(rowTitle, items, imgWdthopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
rowTitlestring
itemsdynamic
imgWdthdynamic<optional>
0
Returns:
Type: 
dynamic

(static) init() → {void}

Returns:
Type: 
void

(static) loadParts(data) → {void}

Parameters:
NameTypeDescription
dataobject
Returns:
Type: 
void

(static) loadPersonVideos(personId) → {void}

Parameters:
NameTypeDescription
personIddynamic
Returns:
Type: 
void

(static) onAdditionalPartsLoaded() → {void}

Returns:
Type: 
void

(static) onLikeThisLoaded() → {void}

Returns:
Type: 
void

(static) onMoviesLoaded() → {void}

Returns:
Type: 
void

(static) onPeopleLoaded() → {void}

Returns:
Type: 
void

(static) onRowItemFocused() → {void}

Returns:
Type: 
void

(static) onRowItemSelected() → {void}

Returns:
Type: 
void

(static) onSeriesLoaded() → {void}

Returns:
Type: 
void

(static) onShowsLoaded() → {void}

Returns:
Type: 
void

(static) onSpecialFeaturesLoaded() → {dynamic}

Returns:
Type: 
dynamic

(static) updateSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-FavoriteItemsTask.html b/docs/api/module-FavoriteItemsTask.html new file mode 100644 index 000000000..53e149c0e --- /dev/null +++ b/docs/api/module-FavoriteItemsTask.html @@ -0,0 +1,3 @@ +Module: FavoriteItemsTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-FolderData.html b/docs/api/module-FolderData.html new file mode 100644 index 000000000..6617b2941 --- /dev/null +++ b/docs/api/module-FolderData.html @@ -0,0 +1,3 @@ +Module: FolderData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GetFiltersTask.html b/docs/api/module-GetFiltersTask.html new file mode 100644 index 000000000..1b33f48e6 --- /dev/null +++ b/docs/api/module-GetFiltersTask.html @@ -0,0 +1,3 @@ +Module: GetFiltersTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GetNextEpisodeTask.html b/docs/api/module-GetNextEpisodeTask.html new file mode 100644 index 000000000..207273957 --- /dev/null +++ b/docs/api/module-GetNextEpisodeTask.html @@ -0,0 +1,3 @@ +Module: GetNextEpisodeTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GetPlaybackInfoTask.html b/docs/api/module-GetPlaybackInfoTask.html new file mode 100644 index 000000000..5fddc7e74 --- /dev/null +++ b/docs/api/module-GetPlaybackInfoTask.html @@ -0,0 +1,3 @@ +Module: GetPlaybackInfoTask
On this page

Methods

(static) GetTranscodingStats(deviceSession) → {dynamic}

Parameters:
NameTypeDescription
deviceSessiondynamic
Returns:
Type: 
dynamic

(static) ItemPostPlaybackInfo(id, mediaSourceIdopt, audioTrackIndexopt, startTimeTicksopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
mediaSourceIdstring<optional>
""
audioTrackIndexinteger<optional>
-1
startTimeTickslonginteger<optional>
0
Returns:
Type: 
dynamic

(static) getDisplayBitrate(bitrate) → {dynamic}

Parameters:
NameTypeDescription
bitratedynamic
Returns:
Type: 
dynamic

(static) getPlaybackInfoTask() → {void}

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.

Returns:
Type: 
void

(static) havePlaybackInfo() → {dynamic}

Returns:
Type: 
dynamic

(static) init() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GetShuffleEpisodesTask.html b/docs/api/module-GetShuffleEpisodesTask.html new file mode 100644 index 000000000..168d10b03 --- /dev/null +++ b/docs/api/module-GetShuffleEpisodesTask.html @@ -0,0 +1,3 @@ +Module: GetShuffleEpisodesTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GridItem.html b/docs/api/module-GridItem.html new file mode 100644 index 000000000..82cbabd89 --- /dev/null +++ b/docs/api/module-GridItem.html @@ -0,0 +1,3 @@ +Module: GridItem
On this page

Methods

(static) focusChanged() → {void}

Display or hide title Visibility on focus change

Returns:
Type: 
void

(static) focusChanging() → {void}

Use FocusPercent to animate scaling of Poser Image

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onPosterLoadStatusChanged() → {void}

Hide backdrop and text when poster loaded

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-GridItemSmall.html b/docs/api/module-GridItemSmall.html new file mode 100644 index 000000000..089a27ec0 --- /dev/null +++ b/docs/api/module-GridItemSmall.html @@ -0,0 +1,3 @@ +Module: GridItemSmall
On this page

Methods

(static) focusChanged() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onPosterLoadStatusChanged() → {void}

Hide backdrop and text when poster loaded

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Home.html b/docs/api/module-Home.html new file mode 100644 index 000000000..2dc22c34d --- /dev/null +++ b/docs/api/module-Home.html @@ -0,0 +1,3 @@ +Module: Home
On this page

Methods

(static) OnScreenShown() → {void}

JFScreen hook that gets ran as needed. Used to update the focus, the state of the data, and tells the server about the device profile

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) loadLibraries() → {void}

Returns:
Type: 
void

(static) postFinished() → {void}

Triggered by m.postTask after completing a post. Empty the task data when finished.

Returns:
Type: 
void

(static) refresh() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-HomeData.html b/docs/api/module-HomeData.html new file mode 100644 index 000000000..9d4ac7a41 --- /dev/null +++ b/docs/api/module-HomeData.html @@ -0,0 +1,3 @@ +Module: HomeData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-HomeItem.html b/docs/api/module-HomeItem.html new file mode 100644 index 000000000..a1abc8ed5 --- /dev/null +++ b/docs/api/module-HomeItem.html @@ -0,0 +1,3 @@ +Module: HomeItem
On this page

Methods

(static) drawProgressBar(itemData) → {void}

Draws and animates item progress bar

Parameters:
NameTypeDescription
itemDatadynamic
Returns:
Type: 
void

(static) focusChanged() → {void}

Enable title scrolling based on item Focus

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onPosterLoadStatusChanged() → {void}

Hide backdrop and icon when poster loaded

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-HomeRowItemSizes.html b/docs/api/module-HomeRowItemSizes.html new file mode 100644 index 000000000..142d31c53 --- /dev/null +++ b/docs/api/module-HomeRowItemSizes.html @@ -0,0 +1,3 @@ +Module: HomeRowItemSizes
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-HomeRows.html b/docs/api/module-HomeRows.html new file mode 100644 index 000000000..58fa1a182 --- /dev/null +++ b/docs/api/module-HomeRows.html @@ -0,0 +1,3 @@ +Module: HomeRows
On this page

Methods

(static) addHomeSection(sectionType) → {boolean}

addHomeSection: Adds a new home section to the home rows.

Parameters:
NameTypeDescription
sectionTypestring

Type of section to add

Returns:
  • indicating if the section was handled
Type: 
boolean

(static) createContinueWatchingRow() → {void}

createContinueWatchingRow: Creates a row displaying items the user can continue watching

Returns:
Type: 
void

(static) createFavoritesRow() → {void}

createFavoritesRow: Creates a row displaying items from the user's favorites list

Returns:
Type: 
void

(static) createLatestInRows() → {void}

createLatestInRows: Creates a row displaying latest items in each of the user's libraries

Returns:
Type: 
void

(static) createLibraryRow() → {void}

createLibraryRow: Creates a row displaying the user's libraries

Returns:
Type: 
void

(static) createLiveTVRow() → {void}

createLiveTVRow: Creates a row displaying the live tv now on section

Returns:
Type: 
void

(static) createNextUpRow() → {void}

createNextUpRow: Creates a row displaying next episodes up to watch

Returns:
Type: 
void

(static) filterNodeArray(nodeArray, nodeKey, excludeArray) → {object}

Parameters:
NameTypeDescription
nodeArrayobject
nodeKeystring
excludeArrayobject
Returns:
Type: 
object

(static) getOriginalSectionIndex(sectionName) → {integer}

getOriginalSectionIndex: Gets the index of a section from user settings and adds count of currently known latest media sections

Parameters:
NameTypeDescription
sectionNamestring

Name of section we're looking up

Returns:
  • indicating index of section taking latest media sections into account
Type: 
integer

(static) getSectionIndex(sectionTitle) → {integer}

getSectionIndex: Returns index of requested section in home row content

Parameters:
NameTypeDescription
sectionTitlestring

Title of section we're checking for

Returns:
  • indicating index of request section
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) itemSelected() → {void}

Returns:
Type: 
void

(static) loadLibraries() → {void}

Returns:
Type: 
void

(static) loadingTimerComplete() → {void}

loadingTimerComplete: Event handler for when loading wait time has expired

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onLibrariesLoaded() → {void}

onLibrariesLoaded: Handler when LoadLibrariesTask returns data

Returns:
Type: 
void

(static) processUserSections() → {void}

processUserSections: Loop through user's chosen home section settings and generate the content for each row

Returns:
Type: 
void

(static) removeHomeSection(sectionToRemove, sectionTitleToRemove) → {void}

removeHomeSection: Removes a home section from the home rows

Parameters:
NameTypeDescription
sectionToRemovestring

Title property of section we're removing

sectionTitleToRemovestring
Returns:
Type: 
void

(static) sectionExists(sectionTitle) → {boolean}

sectionExists: Checks if passed section exists in home row content

Parameters:
NameTypeDescription
sectionTitlestring

Title of section we're checking for

Returns:
  • indicating if the section currently exists in the home row content
Type: 
boolean

(static) setRowItemSize() → {void}

setRowItemSize: Loops through all home sections and sets the correct item sizes per row

Returns:
Type: 
void

(static) updateContinueWatchingItems() → {void}

updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed

Returns:
Type: 
void

(static) updateFavoritesItems() → {void}

updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed

Returns:
Type: 
void

(static) updateHomeRows() → {void}

updateHomeRows: Update function exposed to outside components

Returns:
Type: 
void

(static) updateLatestItems(msg) → {void}

updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed

Parameters:
NameTypeDescription
msgdynamic

LoadItemsTask

Returns:
Type: 
void

(static) updateNextUpItems() → {void}

updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed

Returns:
Type: 
void

(static) updateOnNowItems() → {void}

updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed

Returns:
Type: 
void

(static) updateSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-IconButton.html b/docs/api/module-IconButton.html new file mode 100644 index 000000000..3e2d42d91 --- /dev/null +++ b/docs/api/module-IconButton.html @@ -0,0 +1,3 @@ +Module: IconButton
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onBackgroundChanged() → {void}

Returns:
Type: 
void

(static) onFocusChanged() → {void}

Returns:
Type: 
void

(static) onHeightChanged() → {void}

Returns:
Type: 
void

(static) onIconChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onPaddingChanged() → {void}

Returns:
Type: 
void

(static) onTextChanged() → {void}

Returns:
Type: 
void

(static) onWidthChanged() → {void}

Returns:
Type: 
void

(static) setIconSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Image.html b/docs/api/module-Image.html new file mode 100644 index 000000000..6031fc21e --- /dev/null +++ b/docs/api/module-Image.html @@ -0,0 +1,3 @@ +Module: Image
On this page

Methods

(static) ImageURL(id, versionopt, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
iddynamic
versiondynamic<optional>
"Primary"
paramsdynamic<optional>
{}
Returns:
Type: 
dynamic

(static) ItemImages(idopt, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring<optional>
""
paramsobject<optional>
{}
Returns:
Type: 
dynamic

(static) PosterImage(id, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
paramsobject<optional>
{}
Returns:
Type: 
dynamic

(static) UserImageURL(id, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
iddynamic
paramsdynamic<optional>
{}
Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ImageData.html b/docs/api/module-ImageData.html new file mode 100644 index 000000000..8fd62ee04 --- /dev/null +++ b/docs/api/module-ImageData.html @@ -0,0 +1,3 @@ +Module: ImageData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-IntegerKeyboard.html b/docs/api/module-IntegerKeyboard.html new file mode 100644 index 000000000..170a000e0 --- /dev/null +++ b/docs/api/module-IntegerKeyboard.html @@ -0,0 +1,3 @@ +Module: IntegerKeyboard
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ItemGrid.html b/docs/api/module-ItemGrid.html new file mode 100644 index 000000000..b09c06256 --- /dev/null +++ b/docs/api/module-ItemGrid.html @@ -0,0 +1,3 @@ +Module: ItemGrid
On this page

Methods

(static) ItemDataLoaded(msg) → {void}

Handle loaded data, and add to Grid

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) SetBackground(backgroundUri) → {void}

Set Background Image

Parameters:
NameTypeDescription
backgroundUristring
Returns:
Type: 
void

(static) SetUpOptions() → {void}

Data to display when options button selected

Returns:
Type: 
void

(static) getCollectionType() → {string}

Return parent collection type

Returns:
Type: 
string

(static) getItemFocused() → {dynamic}

Returns Focused Item

Returns:
Type: 
dynamic

(static) inStringArray(array, searchValue) → {boolean}

Search string array for search value. Return if it's found

Parameters:
NameTypeDescription
arraydynamic
searchValuedynamic
Returns:
Type: 
boolean

(static) init() → {void}

Returns:
Type: 
void

(static) loadInitialItems() → {void}

Load initial set of Data

Returns:
Type: 
void

(static) loadMoreData() → {void}

Load next set of items

Returns:
Type: 
void

(static) newBGLoaded() → {void}

When Image Loading Status changes

Returns:
Type: 
void

(static) onChannelFocused(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onChannelSelected(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onGenreItemSelected() → {void}

Genre Item Selected

Returns:
Type: 
void

(static) onItemFocused() → {void}

Handle new item being focused

Returns:
Type: 
void

(static) onItemSelected() → {void}

Item Selected

Returns:
Type: 
void

(static) onItemalphaSelected() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onvoiceFilter() → {void}

Returns:
Type: 
void

(static) optionsClosed() → {void}

Check if options updated and any reloading required

Returns:
Type: 
void

(static) setBoxsetsOptions(options) → {void}

Set Boxset view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setDefaultOptions(options) → {void}

Set Default view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setLiveTvOptions(options) → {void}

Set Live TV view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setMoviesOptions(options) → {void}

Set Movies view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setMusicOptions(options) → {void}

Set Music view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setPhotoAlbumOptions(options) → {void}

Set Photo Album view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setTvShowsOptions(options) → {void}

Set TV Show view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) showTVGuide() → {void}

Returns:
Type: 
void

(static) swapDone() → {void}

Swap Complete

Returns:
Type: 
void

(static) updateTitle() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ItemGridOptions.html b/docs/api/module-ItemGridOptions.html new file mode 100644 index 000000000..5fee59000 --- /dev/null +++ b/docs/api/module-ItemGridOptions.html @@ -0,0 +1,3 @@ +Module: ItemGridOptions
On this page

Methods

(static) buttonFocusChanged() → {void}

Switch menu shown when button focus changes

Returns:
Type: 
void

(static) hideChecklist() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isFilterMenuDataValid() → {boolean}

Check if data for Filter Menu is valid

Returns:
Type: 
boolean

(static) onFilterFocusChange() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) optionsSet() → {void}

Returns:
Type: 
void

(static) saveFavoriteItemSelected(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) setHeartColor(color) → {void}

Parameters:
NameTypeDescription
colorstring
Returns:
Type: 
void

(static) showChecklist() → {void}

Returns:
Type: 
void

(static) toggleFavorite() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Items.html b/docs/api/module-Items.html new file mode 100644 index 000000000..b89173842 --- /dev/null +++ b/docs/api/module-Items.html @@ -0,0 +1,3 @@ +Module: Items
On this page

Methods

(static) AppearsOnList(id) → {dynamic}

Get list of albums an artist appears on

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) ArtistOverview(name) → {dynamic}

Music Artist Data

Parameters:
NameTypeDescription
namestring
Returns:
Type: 
dynamic

(static) AudioItem(id) → {dynamic}

Get Songs that are on an Album

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) AudioStream(id) → {dynamic}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) BackdropImage(id) → {dynamic}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) CreateArtistMix(id) → {dynamic}

Get Instant Mix based on item

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) CreateInstantMix(id) → {dynamic}

Get Instant Mix based on item

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) GetIntroVideos(id) → {dynamic}

Get Intro Videos for an item

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) GetSongsByArtist(id, paramsopt) → {dynamic}

Get list of songs belonging to an artist

Parameters:
NameTypeAttributesDefaultDescription
idstring
paramsobject<optional>
{}
Returns:
Type: 
dynamic

(static) ItemGetPlaybackInfo(id, startTimeTicksopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
startTimeTickslonginteger<optional>
0
Returns:
Type: 
dynamic

(static) ItemMetaData(id) → {dynamic}

MetaData about an item

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) ItemPostPlaybackInfo(id, mediaSourceIdopt, audioTrackIndexopt, subtitleTrackIndexopt, startTimeTicksopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
mediaSourceIdstring<optional>
""
audioTrackIndexinteger<optional>
-1
subtitleTrackIndexinteger<optional>
-1
startTimeTickslonginteger<optional>
0
Returns:
Type: 
dynamic

(static) MusicAlbumList(id) → {dynamic}

Get list of albums belonging to an artist

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) MusicSongList(id) → {dynamic}

Get Songs that are on an Album

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) PlaylistItemList(id) → {dynamic}

Get Items that are under the provided item

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) TVEpisodeShuffleList(show_id) → {dynamic}

Parameters:
NameTypeDescription
show_idstring
Returns:
Type: 
dynamic

(static) TVEpisodes(showId, seasonId) → {dynamic}

Returns a list of TV Shows for a given TV Show and season Accepts strings for the TV Show Id and the season Id

Parameters:
NameTypeDescription
showIdstring
seasonIdstring
Returns:
Type: 
dynamic

(static) TVSeasonExtras(seasonId) → {dynamic}

Returns a list of extra features for a TV Show season Accepts a string that is a TV Show season id

Parameters:
NameTypeDescription
seasonIdstring
Returns:
Type: 
dynamic

(static) TVSeasons(id) → {dynamic}

Seasons for a TV Show

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) searchMedia(query) → {dynamic}

Search across all libraries

Parameters:
NameTypeDescription
querystring
Returns:
Type: 
dynamic

(static) useTranscodeAudioStream(playbackInfo) → {dynamic}

Parameters:
NameTypeDescription
playbackInfodynamic
Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFButton.html b/docs/api/module-JFButton.html new file mode 100644 index 000000000..271fc6bb8 --- /dev/null +++ b/docs/api/module-JFButton.html @@ -0,0 +1,3 @@ +Module: JFButton
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFButtons.html b/docs/api/module-JFButtons.html new file mode 100644 index 000000000..58e022f77 --- /dev/null +++ b/docs/api/module-JFButtons.html @@ -0,0 +1,3 @@ +Module: JFButtons
On this page

Methods

(static) focusChanged() → {void}

Change opacity of the highlighted menu item based on focus

Returns:
Type: 
void

(static) highlightSelected(index, animateopt) → {void}

Highlight selected menu option

Parameters:
NameTypeAttributesDefaultDescription
indexinteger
animatedynamic<optional>
true
Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) renderChanged() → {void}

When options are fully displayed, set focus and selected option

Returns:
Type: 
void

(static) selectedIndexChanged() → {void}

When Selected Index set, ensure it is the one Focused

Returns:
Type: 
void

(static) showButtons() → {void}

Returns:
Type: 
void

(static) updateButtons() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFGroup.html b/docs/api/module-JFGroup.html new file mode 100644 index 000000000..cf656e9cd --- /dev/null +++ b/docs/api/module-JFGroup.html @@ -0,0 +1,3 @@ +Module: JFGroup
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFMessageDialog.html b/docs/api/module-JFMessageDialog.html new file mode 100644 index 000000000..9d3497b27 --- /dev/null +++ b/docs/api/module-JFMessageDialog.html @@ -0,0 +1,3 @@ +Module: JFMessageDialog
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) redraw() → {void}

Returns:
Type: 
void

(static) updateMessage() → {void}

Returns:
Type: 
void

(static) updateOptions() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFOverhang.html b/docs/api/module-JFOverhang.html new file mode 100644 index 000000000..6ce57322b --- /dev/null +++ b/docs/api/module-JFOverhang.html @@ -0,0 +1,3 @@ +Module: JFOverhang
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onVisibleChange() → {void}

Returns:
Type: 
void

(static) resetTime() → {void}

Returns:
Type: 
void

(static) setClockVisibility() → {void}

Returns:
Type: 
void

(static) setRightSeperatorVisibility() → {void}

Returns:
Type: 
void

(static) updateOptions() → {void}

Returns:
Type: 
void

(static) updateTime() → {void}

Returns:
Type: 
void

(static) updateTimeDisplay() → {void}

Returns:
Type: 
void

(static) updateTitle() → {void}

Returns:
Type: 
void

(static) updateUser() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFScene.html b/docs/api/module-JFScene.html new file mode 100644 index 000000000..722aae41f --- /dev/null +++ b/docs/api/module-JFScene.html @@ -0,0 +1,3 @@ +Module: JFScene
On this page

Methods

(static) disableRemoteChanged() → {void}

Triggered when the disableRemote boolean component field is changed

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isLoadingChanged() → {void}

Triggered when the isLoading boolean component field is changed

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFScreen.html b/docs/api/module-JFScreen.html new file mode 100644 index 000000000..79c17a25d --- /dev/null +++ b/docs/api/module-JFScreen.html @@ -0,0 +1,3 @@ +Module: JFScreen
On this page

Methods

(static) OnScreenHidden() → {void}

Function called when the screen is hidden by the screen manager It is expected that screens override this function if required, to handle focus any actions required on the screen being hidden

Returns:
Type: 
void

(static) OnScreenShown() → {void}

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

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFServer.html b/docs/api/module-JFServer.html new file mode 100644 index 000000000..6d1b53b53 --- /dev/null +++ b/docs/api/module-JFServer.html @@ -0,0 +1,3 @@ +Module: JFServer
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onFocusPercentChange(event) → {void}

Parameters:
NameTypeDescription
eventdynamic
Returns:
Type: 
void

(static) setTextColor(percentFocused) → {void}

Parameters:
NameTypeDescription
percentFocuseddynamic
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-JFVideo.html b/docs/api/module-JFVideo.html new file mode 100644 index 000000000..c09448416 --- /dev/null +++ b/docs/api/module-JFVideo.html @@ -0,0 +1,3 @@ +Module: JFVideo
On this page

Methods

(static) ReportPlayback(stateopt) → {void}

Report playback to server

Parameters:
NameTypeAttributesDefaultDescription
statestring<optional>
"update"
Returns:
Type: 
void

(static) bufferCheck(msg) → {void}

Check the the buffering has not hung

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) checkTimeToDisplayNextEpisode() → {void}

Checks if we need to display the Next Episode button

Returns:
Type: 
void

(static) hideNextEpisodeButton() → {void}

Runs hide Next Episode button animation and sets focus back to video

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) loadCaption() → {void}

Returns:
Type: 
void

(static) onAllowCaptionsChange() → {void}

Returns:
Type: 
void

(static) onContentChange() → {void}

Event handler for when video content field changes

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onNextEpisodeDataLoaded() → {void}

Returns:
Type: 
void

(static) onPositionChanged() → {void}

When Video Player state changes

Returns:
Type: 
void

(static) onState(msg) → {void}

When Video Player state changes

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) showNextEpisodeButton() → {void}

Runs Next Episode button animation and sets focus to button

Returns:
Type: 
void

(static) toggleCaption() → {void}

Returns:
Type: 
void

(static) updateCaption() → {void}

Returns:
Type: 
void

(static) updateCount() → {void}

Update count down text

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ListPoster.html b/docs/api/module-ListPoster.html new file mode 100644 index 000000000..63aeb1f26 --- /dev/null +++ b/docs/api/module-ListPoster.html @@ -0,0 +1,3 @@ +Module: ListPoster
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadChannelsTask.html b/docs/api/module-LoadChannelsTask.html new file mode 100644 index 000000000..e04d9f40e --- /dev/null +++ b/docs/api/module-LoadChannelsTask.html @@ -0,0 +1,3 @@ +Module: LoadChannelsTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadItemsTask.html b/docs/api/module-LoadItemsTask.html new file mode 100644 index 000000000..498c58cfa --- /dev/null +++ b/docs/api/module-LoadItemsTask.html @@ -0,0 +1,3 @@ +Module: LoadItemsTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadItemsTask2.html b/docs/api/module-LoadItemsTask2.html new file mode 100644 index 000000000..d19c3be1f --- /dev/null +++ b/docs/api/module-LoadItemsTask2.html @@ -0,0 +1,3 @@ +Module: LoadItemsTask2
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadPhotoTask.html b/docs/api/module-LoadPhotoTask.html new file mode 100644 index 000000000..0e46c43d9 --- /dev/null +++ b/docs/api/module-LoadPhotoTask.html @@ -0,0 +1,3 @@ +Module: LoadPhotoTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadProgramDetailsTask.html b/docs/api/module-LoadProgramDetailsTask.html new file mode 100644 index 000000000..9e6c17517 --- /dev/null +++ b/docs/api/module-LoadProgramDetailsTask.html @@ -0,0 +1,3 @@ +Module: LoadProgramDetailsTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadScreenSaverTimeoutTask.html b/docs/api/module-LoadScreenSaverTimeoutTask.html new file mode 100644 index 000000000..ef8d85f3b --- /dev/null +++ b/docs/api/module-LoadScreenSaverTimeoutTask.html @@ -0,0 +1,3 @@ +Module: LoadScreenSaverTimeoutTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadSheduleTask.html b/docs/api/module-LoadSheduleTask.html new file mode 100644 index 000000000..41a329dff --- /dev/null +++ b/docs/api/module-LoadSheduleTask.html @@ -0,0 +1,3 @@ +Module: LoadSheduleTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoadVideoContentTask.html b/docs/api/module-LoadVideoContentTask.html new file mode 100644 index 000000000..92b142c06 --- /dev/null +++ b/docs/api/module-LoadVideoContentTask.html @@ -0,0 +1,3 @@ +Module: LoadVideoContentTask
On this page

Methods

(static) FindPreferredAudioStream(streams) → {integer}

Parameters:
NameTypeDescription
streamsdynamic
Returns:
Type: 
integer

(static) LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idxopt, subtitle_idxopt, forceTranscodingopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
videoobject
mediaSourceIddynamic
audio_stream_idxinteger<optional>
1
subtitle_idxinteger<optional>
-1
forceTranscodingboolean<optional>
false
Returns:
Type: 
void

(static) LoadItems_VideoPlayer(id, mediaSourceIdopt, audio_stream_idxopt, subtitle_idxopt, forceTranscodingopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
mediaSourceIddynamic<optional>
invalid
audio_stream_idxinteger<optional>
1
subtitle_idxinteger<optional>
-1
forceTranscodingboolean<optional>
false
Returns:
Type: 
dynamic

(static) addNextEpisodesToQueue(showID) → {void}

Add next episodes to the playback queue

Parameters:
NameTypeDescription
showIDdynamic
Returns:
Type: 
void

(static) addSubtitlesToVideo(video, meta) → {void}

Parameters:
NameTypeDescription
videodynamic
metadynamic
Returns:
Type: 
void

(static) addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) → {void}

Parameters:
NameTypeDescription
videodynamic
mediaSourceIddynamic
audio_stream_idxdynamic
fully_externaldynamic
Returns:
Type: 
void

(static) directPlaySupported(meta) → {boolean}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
boolean

(static) getContainerType(meta) → {string}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
string

(static) getSubtitleLanguages() → {dynamic}

Returns:
Type: 
dynamic

(static) getTranscodeReasons(url) → {object}

Extract array of Transcode Reasons from the content URL

Parameters:
NameTypeDescription
urlstring
Returns:
  • Array of Strings
Type: 
object

(static) init() → {void}

Returns:
Type: 
void

(static) loadItems() → {void}

Returns:
Type: 
void

(static) sortSubtitles(id, MediaStreams) → {dynamic}

Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top

Parameters:
NameTypeDescription
idstring
MediaStreamsdynamic
Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-LoginScene.html b/docs/api/module-LoginScene.html new file mode 100644 index 000000000..a9afaafe8 --- /dev/null +++ b/docs/api/module-LoginScene.html @@ -0,0 +1,3 @@ +Module: LoginScene
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Main.html b/docs/api/module-Main.html new file mode 100644 index 000000000..9643ed845 --- /dev/null +++ b/docs/api/module-Main.html @@ -0,0 +1,3 @@ +Module: Main
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MovieData.html b/docs/api/module-MovieData.html new file mode 100644 index 000000000..460058aaa --- /dev/null +++ b/docs/api/module-MovieData.html @@ -0,0 +1,3 @@ +Module: MovieData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MovieDetails.html b/docs/api/module-MovieDetails.html new file mode 100644 index 000000000..56f248995 --- /dev/null +++ b/docs/api/module-MovieDetails.html @@ -0,0 +1,3 @@ +Module: MovieDetails
On this page

Methods

(static) OnScreenShown() → {void}

Returns:
Type: 
void

(static) SetDefaultAudioTrack(itemData) → {void}

Parameters:
NameTypeDescription
itemDatadynamic
Returns:
Type: 
void

(static) SetUpAudioOptions(streams) → {void}

Parameters:
NameTypeDescription
streamsdynamic
Returns:
Type: 
void

(static) SetUpVideoOptions(streams) → {void}

Parameters:
NameTypeDescription
streamsdynamic
Returns:
Type: 
void

(static) audioOptionsClosed() → {void}

Check if options updated and any reloading required

Returns:
Type: 
void

(static) getEndTime() → {string}

Returns:
Type: 
string

(static) getRuntime() → {integer}

Returns:
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) round(f) → {integer}

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) setFavoriteColor() → {void}

Returns:
Type: 
void

(static) setFieldText(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void

(static) setWatchedColor() → {void}

Returns:
Type: 
void

(static) trailerAvailableChanged() → {void}

Returns:
Type: 
void

(static) videoOptionsClosed() → {void}

Check if options were updated and if any reloding is needed...

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MovieLibraryView.html b/docs/api/module-MovieLibraryView.html new file mode 100644 index 000000000..7f69b3c69 --- /dev/null +++ b/docs/api/module-MovieLibraryView.html @@ -0,0 +1,3 @@ +Module: MovieLibraryView
On this page

Methods

(static) FilterDataLoaded(msg) → {void}

Logo Image Loaded Event Handler

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) ItemDataLoaded(msg) → {void}

Handle loaded data, and add to Grid

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) LogoImageLoaded(msg) → {void}

Logo Image Loaded Event Handler

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) OnScreenHidden() → {void}

Returns:
Type: 
void

(static) OnScreenShown() → {void}

Returns:
Type: 
void

(static) SetBackground(backgroundUri) → {void}

Set Background Image

Parameters:
NameTypeDescription
backgroundUristring
Returns:
Type: 
void

(static) SetName(movieName) → {void}

Set Selected Movie Name

Parameters:
NameTypeDescription
movieNamestring
Returns:
Type: 
void

(static) SetOfficialRating(movieOfficialRating) → {void}

Set Selected Movie OfficialRating

Parameters:
NameTypeDescription
movieOfficialRatingstring
Returns:
Type: 
void

(static) SetOverview(movieOverview) → {void}

Set Selected Movie Overview

Parameters:
NameTypeDescription
movieOverviewstring
Returns:
Type: 
void

(static) SetProductionYear(movieProductionYear) → {void}

Set Selected Movie ProductionYear

Parameters:
NameTypeDescription
movieProductionYeardynamic
Returns:
Type: 
void

(static) getCollectionType() → {string}

Return parent collection type

Returns:
Type: 
string

(static) getItemFocused() → {dynamic}

Returns Focused Item

Returns:
Type: 
dynamic

(static) getRuntime(runTimeTicks) → {integer}

Parameters:
NameTypeDescription
runTimeTicksdynamic
Returns:
Type: 
integer

(static) inStringArray(array, searchValue) → {boolean}

Search string array for search value. Return if it's found

Parameters:
NameTypeDescription
arraydynamic
searchValuedynamic
Returns:
Type: 
boolean

(static) init() → {void}

Returns:
Type: 
void

(static) loadInitialItems() → {void}

Load initial set of Data

Returns:
Type: 
void

(static) loadMoreData() → {void}

Load next set of items

Returns:
Type: 
void

(static) newBGLoaded() → {void}

When Image Loading Status changes

Returns:
Type: 
void

(static) onChannelSelected(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onGenreItemSelected() → {void}

Genre Item Selected

Returns:
Type: 
void

(static) onItemFocused() → {void}

Handle new item being focused

Returns:
Type: 
void

(static) onItemSelected() → {void}

Item Selected

Returns:
Type: 
void

(static) onItemalphaSelected() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onvoiceFilter() → {void}

Returns:
Type: 
void

(static) optionsClosed() → {void}

Check if options updated and any reloading required

Returns:
Type: 
void

(static) round(f) → {integer}

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) setFieldText(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void

(static) setMoviesOptions(options) → {void}

Set Movies view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setSelectedOptions(options) → {void}

Data to display when options button selected

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setupNodes() → {void}

Returns:
Type: 
void

(static) swapDone() → {void}

Swap Complete

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MovieOptions.html b/docs/api/module-MovieOptions.html new file mode 100644 index 000000000..7305b2e7d --- /dev/null +++ b/docs/api/module-MovieOptions.html @@ -0,0 +1,3 @@ +Module: MovieOptions
On this page

Methods

(static) buttonFocusChanged() → {void}

Switch menu shown when button focus changes

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) optionsSet() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicAlbumData.html b/docs/api/module-MusicAlbumData.html new file mode 100644 index 000000000..2e953f475 --- /dev/null +++ b/docs/api/module-MusicAlbumData.html @@ -0,0 +1,3 @@ +Module: MusicAlbumData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicAlbumSongListData.html b/docs/api/module-MusicAlbumSongListData.html new file mode 100644 index 000000000..9dded7741 --- /dev/null +++ b/docs/api/module-MusicAlbumSongListData.html @@ -0,0 +1,3 @@ +Module: MusicAlbumSongListData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicArtistData.html b/docs/api/module-MusicArtistData.html new file mode 100644 index 000000000..8255a1e89 --- /dev/null +++ b/docs/api/module-MusicArtistData.html @@ -0,0 +1,3 @@ +Module: MusicArtistData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicArtistGridItem.html b/docs/api/module-MusicArtistGridItem.html new file mode 100644 index 000000000..af14799ac --- /dev/null +++ b/docs/api/module-MusicArtistGridItem.html @@ -0,0 +1,3 @@ +Module: MusicArtistGridItem
On this page

Methods

(static) focusChanged() → {void}

Display or hide title Visibility on focus change

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onPosterLoadStatusChanged() → {void}

Hide backdrop and text when poster loaded

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicLibraryView.html b/docs/api/module-MusicLibraryView.html new file mode 100644 index 000000000..a9b170e86 --- /dev/null +++ b/docs/api/module-MusicLibraryView.html @@ -0,0 +1,3 @@ +Module: MusicLibraryView
On this page

Methods

(static) ItemDataLoaded(msg) → {void}

Handle loaded data, and add to Grid

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) LogoImageLoaded(msg) → {void}

Logo Image Loaded Event Handler

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) OnScreenHidden() → {void}

Returns:
Type: 
void

(static) OnScreenShown() → {void}

Returns:
Type: 
void

(static) SetAlbumCount(totalCount) → {void}

Set Selected Artist Album Count

Parameters:
NameTypeDescription
totalCountdynamic
Returns:
Type: 
void

(static) SetBackground(backgroundUri) → {void}

Set Background Image

Parameters:
NameTypeDescription
backgroundUristring
Returns:
Type: 
void

(static) SetGenres(artistGenres) → {void}

Set Selected Artist Genres

Parameters:
NameTypeDescription
artistGenresdynamic
Returns:
Type: 
void

(static) SetName(artistName) → {void}

Set Selected Artist Name

Parameters:
NameTypeDescription
artistNamestring
Returns:
Type: 
void

(static) SetSongCount(totalCount) → {void}

Set Selected Artist Song Count

Parameters:
NameTypeDescription
totalCountdynamic
Returns:
Type: 
void

(static) SetUpOptions() → {void}

Data to display when options button selected

Returns:
Type: 
void

(static) getCollectionType() → {string}

Return parent collection type

Returns:
Type: 
string

(static) getItemFocused() → {dynamic}

Returns Focused Item

Returns:
Type: 
dynamic

(static) inStringArray(array, searchValue) → {boolean}

Search string array for search value. Return if it's found

Parameters:
NameTypeDescription
arraydynamic
searchValuedynamic
Returns:
Type: 
boolean

(static) init() → {void}

Returns:
Type: 
void

(static) loadInitialItems() → {void}

Load initial set of Data

Returns:
Type: 
void

(static) loadMoreData() → {void}

Load next set of items

Returns:
Type: 
void

(static) newBGLoaded() → {void}

When Image Loading Status changes

Returns:
Type: 
void

(static) onChannelSelected(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onGenreItemFocused() → {void}

Genre Item Focused

Returns:
Type: 
void

(static) onGenreItemSelected() → {void}

Genre Item Selected

Returns:
Type: 
void

(static) onItemFocused() → {void}

Handle new item being focused

Returns:
Type: 
void

(static) onItemSelected() → {void}

Item Selected

Returns:
Type: 
void

(static) onItemalphaSelected() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onvoiceFilter() → {void}

Returns:
Type: 
void

(static) optionsClosed() → {void}

Check if options updated and any reloading required

Returns:
Type: 
void

(static) setFieldText(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void

(static) setMusicOptions(options) → {void}

Set Music view, sort, and filter options

Parameters:
NameTypeDescription
optionsdynamic
Returns:
Type: 
void

(static) setupNodes() → {void}

Returns:
Type: 
void

(static) swapDone() → {void}

Swap Complete

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-MusicSongData.html b/docs/api/module-MusicSongData.html new file mode 100644 index 000000000..f53cacd92 --- /dev/null +++ b/docs/api/module-MusicSongData.html @@ -0,0 +1,3 @@ +Module: MusicSongData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OSD.html b/docs/api/module-OSD.html new file mode 100644 index 000000000..ff3323742 --- /dev/null +++ b/docs/api/module-OSD.html @@ -0,0 +1,3 @@ +Module: OSD
On this page

Methods

(static) inactiveCheck() → {void}

inactiveCheck: Checks if the time since last keypress is greater than or equal to the allowed inactive time of the menu.

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onButtonSelected() → {void}

onButtonSelected: Handler for selection of buttons from the menu.

Returns:
Type: 
void

(static) onFocusChanged() → {void}

onFocusChanged: Handler for changes to the focus of this menu.

Returns:
Type: 
void

(static) onItemTitleTextChanged() → {void}

onItemTitleTextChanged: Handler for changes to m.top.itemTitleText param.

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onPlaybackStateChanged() → {void}

onPlaybackStateChanged: Handler for changes to m.top.playbackState param

Returns:
Type: 
void

(static) onProgressPercentageChanged() → {void}

onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param

Returns:
Type: 
void

(static) onVisibleChanged() → {void}

onVisibleChanged: Handler for changes to the visibility of this menu.

Returns:
Type: 
void

(static) resetFocusToDefaultButton() → {void}

resetFocusToDefaultButton: Reset focus back to the default button

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OptionNode.html b/docs/api/module-OptionNode.html new file mode 100644 index 000000000..f93b1c473 --- /dev/null +++ b/docs/api/module-OptionNode.html @@ -0,0 +1,3 @@ +Module: OptionNode
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OptionsButton.html b/docs/api/module-OptionsButton.html new file mode 100644 index 000000000..cf906b1e0 --- /dev/null +++ b/docs/api/module-OptionsButton.html @@ -0,0 +1,3 @@ +Module: OptionsButton
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OptionsData.html b/docs/api/module-OptionsData.html new file mode 100644 index 000000000..04bb354e2 --- /dev/null +++ b/docs/api/module-OptionsData.html @@ -0,0 +1,3 @@ +Module: OptionsData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OptionsSlider.html b/docs/api/module-OptionsSlider.html new file mode 100644 index 000000000..32d2bf4ac --- /dev/null +++ b/docs/api/module-OptionsSlider.html @@ -0,0 +1,3 @@ +Module: OptionsSlider
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-OverviewDialog.html b/docs/api/module-OverviewDialog.html new file mode 100644 index 000000000..2859acd2d --- /dev/null +++ b/docs/api/module-OverviewDialog.html @@ -0,0 +1,3 @@ +Module: OverviewDialog
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PersonData.html b/docs/api/module-PersonData.html new file mode 100644 index 000000000..08c453550 --- /dev/null +++ b/docs/api/module-PersonData.html @@ -0,0 +1,3 @@ +Module: PersonData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PersonDetails.html b/docs/api/module-PersonDetails.html new file mode 100644 index 000000000..596e2144b --- /dev/null +++ b/docs/api/module-PersonDetails.html @@ -0,0 +1,3 @@ +Module: PersonDetails
On this page

Methods

(static) createDialogPallete() → {void}

Returns:
Type: 
void

(static) createFullDscrDlg() → {void}

Returns:
Type: 
void

(static) dscrShowFocus() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) loadPerson() → {void}

Returns:
Type: 
void

(static) onButtonGroupEscaped() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) setFavoriteColor() → {void}

Returns:
Type: 
void

(static) shortDate(isoDate) → {string}

Parameters:
NameTypeDescription
isoDatedynamic
Returns:
Type: 
string
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PhotoData.html b/docs/api/module-PhotoData.html new file mode 100644 index 000000000..bd70d4ebe --- /dev/null +++ b/docs/api/module-PhotoData.html @@ -0,0 +1,3 @@ +Module: PhotoData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PhotoDetails.html b/docs/api/module-PhotoDetails.html new file mode 100644 index 000000000..5d062e097 --- /dev/null +++ b/docs/api/module-PhotoDetails.html @@ -0,0 +1,3 @@ +Module: PhotoDetails
On this page

Methods

(static) OnScreenHidden() → {void}

JFScreen hook. Used to ensure tasks are stopped

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isRandomChanged() → {void}

isRandom component field has changed

Returns:
Type: 
void

(static) isSlideshowChanged() → {void}

isSlideshow component field has changed

Returns:
Type: 
void

(static) isValidToContinue(index) → {dynamic}

Parameters:
NameTypeDescription
indexinteger
Returns:
Type: 
dynamic

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) nextSlide() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onPhotoLoaded() → {void}

Returns:
Type: 
void

(static) statusUpdate() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PlaybackDialog.html b/docs/api/module-PlaybackDialog.html new file mode 100644 index 000000000..4cbedc4f2 --- /dev/null +++ b/docs/api/module-PlaybackDialog.html @@ -0,0 +1,3 @@ +Module: PlaybackDialog
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PlayedCheckmark.html b/docs/api/module-PlayedCheckmark.html new file mode 100644 index 000000000..b3daa0781 --- /dev/null +++ b/docs/api/module-PlayedCheckmark.html @@ -0,0 +1,3 @@ +Module: PlayedCheckmark
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PlaylistData.html b/docs/api/module-PlaylistData.html new file mode 100644 index 000000000..b19f647d8 --- /dev/null +++ b/docs/api/module-PlaylistData.html @@ -0,0 +1,3 @@ +Module: PlaylistData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PlaylistView.html b/docs/api/module-PlaylistView.html new file mode 100644 index 000000000..b7c2e06cf --- /dev/null +++ b/docs/api/module-PlaylistView.html @@ -0,0 +1,3 @@ +Module: PlaylistView
On this page

Methods

(static) adjustScreenForNoOverview() → {void}

Adjust scene by removing overview node and showing more songs

Returns:
Type: 
void

(static) createDialogPallete() → {void}

Returns:
Type: 
void

(static) createFullDscrDlg() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onDoneLoading() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) pageContentChanged() → {void}

Set values for displayed values on screen

Returns:
Type: 
void

(static) setOnScreenTextValues(json) → {void}

Populate on screen text variables

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setPosterImage(posterURL) → {void}

Set poster image on screen

Parameters:
NameTypeDescription
posterURLdynamic
Returns:
Type: 
void

(static) setScreenTitle(json) → {void}

Set screen's title text

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) setupMainNode() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PlaystateTask.html b/docs/api/module-PlaystateTask.html new file mode 100644 index 000000000..5d11351a4 --- /dev/null +++ b/docs/api/module-PlaystateTask.html @@ -0,0 +1,3 @@ +Module: PlaystateTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PostTask.html b/docs/api/module-PostTask.html new file mode 100644 index 000000000..dc80ae7bd --- /dev/null +++ b/docs/api/module-PostTask.html @@ -0,0 +1,3 @@ +Module: PostTask
On this page

Methods

(static) asyncPost(req, dataopt) → {integer}

Post data and wait for response code

Parameters:
NameTypeAttributesDefaultDescription
reqdynamic
datastring<optional>
""
Returns:
Type: 
integer

(static) empty() → {void}

Revert PostTask to default state

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) postItems() → {void}

Main function for PostTask. Posts either an array of data or a string of data to an API endpoint. Saves the response information

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ProgramDetails.html b/docs/api/module-ProgramDetails.html new file mode 100644 index 000000000..36f3053d8 --- /dev/null +++ b/docs/api/module-ProgramDetails.html @@ -0,0 +1,3 @@ +Module: ProgramDetails
On this page

Methods

(static) channelUpdated() → {void}

Returns:
Type: 
void

(static) focusChanged() → {void}

Show view channel button when item has Focus

Returns:
Type: 
void

(static) getDurationStringFromSeconds(seconds) → {string}

Get program duration string (e.g. 1h 20m)

Parameters:
NameTypeDescription
secondsdynamic
Returns:
Type: 
string

(static) getRelativeDayName(date) → {string}

Get relative date name for a date (yesterday, today, tomorrow, or otherwise weekday name )

Parameters:
NameTypeDescription
datedynamic
Returns:
Type: 
string

(static) init() → {void}

Returns:
Type: 
void

(static) onAnimationComplete() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) programUpdated() → {void}

Returns:
Type: 
void

(static) setupLabels() → {void}

Set up Live and Repeat label sizes

Returns:
Type: 
void

(static) updateLabels(recordTextopt, recordSeriesTextopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
recordTextdynamic<optional>
"Record"
recordSeriesTextdynamic<optional>
"Record Series"
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-PublicUserData.html b/docs/api/module-PublicUserData.html new file mode 100644 index 000000000..8776d0313 --- /dev/null +++ b/docs/api/module-PublicUserData.html @@ -0,0 +1,3 @@ +Module: PublicUserData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-QueueManager.html b/docs/api/module-QueueManager.html new file mode 100644 index 000000000..dd0eb6389 --- /dev/null +++ b/docs/api/module-QueueManager.html @@ -0,0 +1,3 @@ +Module: QueueManager
On this page

Methods

(static) clear() → {void}

Clear all content from play queue

Returns:
Type: 
void

(static) clearHold() → {void}

Clear all hold content

Returns:
Type: 
void

(static) deleteAtIndex(index) → {void}

Delete item from play queue at passed index

Parameters:
NameTypeDescription
indexdynamic
Returns:
Type: 
void

(static) getCount() → {dynamic}

Return the number of items in the play queue

Returns:
Type: 
dynamic

(static) getCurrentItem() → {dynamic}

Return the item currently in focus from the play queue

Returns:
Type: 
dynamic

(static) getHold() → {dynamic}

Return the items in the hold

Returns:
Type: 
dynamic

(static) getIsShuffled() → {dynamic}

Return whether or not shuffle is enabled

Returns:
Type: 
dynamic

(static) getItemByIndex(index) → {dynamic}

Return the item in the passed index from the play queue

Parameters:
NameTypeDescription
indexdynamic
Returns:
Type: 
dynamic

(static) getItemType(item) → {string}

Parameters:
NameTypeDescription
itemdynamic
Returns:
Type: 
string

(static) getPosition() → {dynamic}

Returns current playback position within the queue

Returns:
Type: 
dynamic

(static) getQueue() → {dynamic}

Return the current play queue

Returns:
Type: 
dynamic

(static) getQueueTypes() → {dynamic}

Return the types of items in current play queue

Returns:
Type: 
dynamic

(static) getQueueUniqueTypes() → {dynamic}

Return the unique types of items in current play queue

Returns:
Type: 
dynamic

(static) getUnshuffledQueue() → {dynamic}

Return original, unshuffled queue

Returns:
Type: 
dynamic

(static) hold(newItem) → {void}

Hold an item

Parameters:
NameTypeDescription
newItemdynamic
Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isPrerollActive() → {boolean}

Return isPrerollActive status

Returns:
Type: 
boolean

(static) moveBack() → {void}

Move queue position back one

Returns:
Type: 
void

(static) moveForward() → {void}

Move queue position ahead one

Returns:
Type: 
void

(static) peek() → {dynamic}

Return item at end of play queue without removing

Returns:
Type: 
dynamic

(static) playQueue() → {void}

Play items in queue

Returns:
Type: 
void

(static) pop() → {void}

Remove item at end of play queue

Returns:
Type: 
void

(static) push(newItem) → {void}

Push new items to the play queue

Parameters:
NameTypeDescription
newItemdynamic
Returns:
Type: 
void

(static) resetQueueItemOrder() → {void}

Reset queue items back to original, unshuffled order

Returns:
Type: 
void

(static) resetShuffle() → {void}

Reset shuffle to off state

Returns:
Type: 
void

(static) set(items) → {void}

Replace play queue with passed array

Parameters:
NameTypeDescription
itemsdynamic
Returns:
Type: 
void

(static) setPosition(newPosition) → {void}

Set the queue position

Parameters:
NameTypeDescription
newPositiondynamic
Returns:
Type: 
void

(static) setPrerollStatus(newStatus) → {void}

Set prerollActive status

Parameters:
NameTypeDescription
newStatusboolean
Returns:
Type: 
void

(static) setTopStartingPoint(positionTicks) → {void}

Set starting point for top item in the queue

Parameters:
NameTypeDescription
positionTicksdynamic
Returns:
Type: 
void

(static) shuffleQueueItems() → {void}

Save a copy of the original queue and randomize order of queue items

Returns:
Type: 
void

(static) toggleShuffle() → {void}

Toggle shuffleEnabled state

Returns:
Type: 
void

(static) top() → {dynamic}

Return the fitst item in the play queue

Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-QuickConnect.html b/docs/api/module-QuickConnect.html new file mode 100644 index 000000000..d907ef7b5 --- /dev/null +++ b/docs/api/module-QuickConnect.html @@ -0,0 +1,3 @@ +Module: QuickConnect
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-QuickConnectDialog.html b/docs/api/module-QuickConnectDialog.html new file mode 100644 index 000000000..2835845f3 --- /dev/null +++ b/docs/api/module-QuickConnectDialog.html @@ -0,0 +1,3 @@ +Module: QuickConnectDialog
On this page

Methods

(static) OnAuthenticated() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onButtonSelected() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) quickConnectClosed() → {void}

Returns:
Type: 
void

(static) quickConnectStatus() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-RadioDialog.html b/docs/api/module-RadioDialog.html new file mode 100644 index 000000000..6075be0d1 --- /dev/null +++ b/docs/api/module-RadioDialog.html @@ -0,0 +1,3 @@ +Module: RadioDialog
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) moveScrollBar() → {void}

Move the popup's scroll bar

Returns:
Type: 
void

(static) onButtonSelected() → {void}

Event handler for when user selected a button

Returns:
Type: 
void

(static) onContentDataChanged() → {void}

Returns:
Type: 
void

(static) onItemFocused() → {void}

Event handler for when user's cursor highlights an option in the option list

Returns:
Type: 
void

(static) onItemSelected() → {void}

Once user selected an item, move cursor down to OK button

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onScrollBarFocus() → {void}

If somehow the scrollbar gains focus, set focus back to the option list

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-RecordProgramTask.html b/docs/api/module-RecordProgramTask.html new file mode 100644 index 000000000..c2d8e80d0 --- /dev/null +++ b/docs/api/module-RecordProgramTask.html @@ -0,0 +1,3 @@ +Module: RecordProgramTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SceneManager.html b/docs/api/module-SceneManager.html new file mode 100644 index 000000000..b7a4a8a97 --- /dev/null +++ b/docs/api/module-SceneManager.html @@ -0,0 +1,3 @@ +Module: SceneManager
On this page

Methods

(static) clearPreviousScene() → {void}

Clear previous scene from group stack

Returns:
Type: 
void

(static) clearScenes() → {void}

Clear all content from group stack

Returns:
Type: 
void

(static) deleteSceneAtIndex(indexopt) → {void}

Delete scene from group stack at passed index

Parameters:
NameTypeAttributesDefaultDescription
indexdynamic<optional>
1
Returns:
Type: 
void

(static) dismissDialog() → {void}

Close currently displayed dialog

Returns:
Type: 
void

(static) getActiveScene() → {object}

Return group at top of stack without removing

Returns:
Type: 
object

(static) init() → {void}

Returns:
Type: 
void

(static) isDialogOpen() → {boolean}

Returns bool indicating if dialog is currently displayed

Returns:
Type: 
boolean

(static) optionClosed() → {void}

Return button the user selected

Returns:
Type: 
void

(static) optionDialog(title, message, buttons) → {void}

Display dialog to user with an OK button

Parameters:
NameTypeDescription
titledynamic
messagedynamic
buttonsdynamic
Returns:
Type: 
void

(static) optionSelected() → {void}

Return button the user selected

Returns:
Type: 
void

(static) popScene() → {void}

Remove the current group and load the last group from the stack

Returns:
Type: 
void

(static) pushScene(newGroup) → {void}

Push a new group onto the stack, replacing the existing group on the screen

Parameters:
NameTypeDescription
newGroupdynamic
Returns:
Type: 
void

(static) radioDialog(title, message) → {void}

Display dialog to user with an OK button

Parameters:
NameTypeDescription
titledynamic
messagedynamic
Returns:
Type: 
void

(static) registerOverhangData(group) → {void}

Register observers for overhang data

Parameters:
NameTypeDescription
groupdynamic
Returns:
Type: 
void

(static) resetTime() → {void}

Reset time

Returns:
Type: 
void

(static) settings() → {void}

Display user/device settings screen

Returns:
Type: 
void

(static) standardDialog(title, message) → {void}

Display dialog to user with an OK button

Parameters:
NameTypeDescription
titledynamic
messagedynamic
Returns:
Type: 
void

(static) unregisterOverhangData(group) → {void}

Remove observers for overhang data

Parameters:
NameTypeDescription
groupdynamic
Returns:
Type: 
void

(static) updateOptions(msg) → {void}

Update options availability

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) updateOverhangTitle(msg) → {void}

Update overhang title

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) updateOverhangVisible(msg) → {void}

Update whether the overhang is visible or not

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) updateUser() → {void}

Update username in overhang

Returns:
Type: 
void

(static) userMessage(title, message) → {void}

Display dialog to user with an OK button

Parameters:
NameTypeDescription
titlestring
messagestring
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ScheduleProgramData.html b/docs/api/module-ScheduleProgramData.html new file mode 100644 index 000000000..986adab75 --- /dev/null +++ b/docs/api/module-ScheduleProgramData.html @@ -0,0 +1,3 @@ +Module: ScheduleProgramData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SearchBox.html b/docs/api/module-SearchBox.html new file mode 100644 index 000000000..ad7abb89f --- /dev/null +++ b/docs/api/module-SearchBox.html @@ -0,0 +1,3 @@ +Module: SearchBox
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SearchData.html b/docs/api/module-SearchData.html new file mode 100644 index 000000000..4de86e321 --- /dev/null +++ b/docs/api/module-SearchData.html @@ -0,0 +1,3 @@ +Module: SearchData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SearchResults.html b/docs/api/module-SearchResults.html new file mode 100644 index 000000000..6cc851cc0 --- /dev/null +++ b/docs/api/module-SearchResults.html @@ -0,0 +1,3 @@ +Module: SearchResults
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) loadResults() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) searchMedias() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SearchRow.html b/docs/api/module-SearchRow.html new file mode 100644 index 000000000..2035e15f5 --- /dev/null +++ b/docs/api/module-SearchRow.html @@ -0,0 +1,3 @@ +Module: SearchRow
On this page

Methods

(static) addRow(data, title, type_filter) → {void}

Parameters:
NameTypeDescription
datadynamic
titledynamic
type_filterdynamic
Returns:
Type: 
void

(static) getData() → {dynamic}

Returns:
Type: 
dynamic

(static) init() → {void}

Returns:
Type: 
void

(static) updateSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SearchTask.html b/docs/api/module-SearchTask.html new file mode 100644 index 000000000..48a8d692f --- /dev/null +++ b/docs/api/module-SearchTask.html @@ -0,0 +1,3 @@ +Module: SearchTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SeriesData.html b/docs/api/module-SeriesData.html new file mode 100644 index 000000000..5471d078c --- /dev/null +++ b/docs/api/module-SeriesData.html @@ -0,0 +1,3 @@ +Module: SeriesData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ServerDiscoveryTask.html b/docs/api/module-ServerDiscoveryTask.html new file mode 100644 index 000000000..82dc4c9c7 --- /dev/null +++ b/docs/api/module-ServerDiscoveryTask.html @@ -0,0 +1,3 @@ +Module: ServerDiscoveryTask
On this page

Methods

(static) AddServer(server) → {void}

Parameters:
NameTypeDescription
serverdynamic
Returns:
Type: 
void

(static) ProcessClientDiscoveryResponse(message) → {void}

Parameters:
NameTypeDescription
messagedynamic
Returns:
Type: 
void

(static) ProcessSSDPResponse(message) → {void}

Parameters:
NameTypeDescription
messagedynamic
Returns:
Type: 
void

(static) SendClientDiscoveryBroadcast() → {void}

Returns:
Type: 
void

(static) SendSSDPBroadcast() → {void}

Returns:
Type: 
void

(static) execute() → {void}

Returns:
Type: 
void

(static) init() → {void}

Task used to discover jellyfin servers on the local network

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SetServerScreen.html b/docs/api/module-SetServerScreen.html new file mode 100644 index 000000000..f65203cba --- /dev/null +++ b/docs/api/module-SetServerScreen.html @@ -0,0 +1,3 @@ +Module: SetServerScreen
On this page

Methods

(static) ScanForServers() → {void}

Returns:
Type: 
void

(static) ScanForServersComplete(event) → {void}

Parameters:
NameTypeDescription
eventdynamic
Returns:
Type: 
void

(static) ShowKeyboard() → {void}

Returns:
Type: 
void

(static) clearErrorMessage() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onDialogButton() → {dynamic}

Returns:
Type: 
dynamic

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ShowScenes.html b/docs/api/module-ShowScenes.html new file mode 100644 index 000000000..d70680c02 --- /dev/null +++ b/docs/api/module-ShowScenes.html @@ -0,0 +1,3 @@ +Module: ShowScenes
On this page

Methods

(static) CreateAlbumView(album) → {dynamic}

Shows details on selected album. Description text, image, and list of available songs

Parameters:
NameTypeDescription
albumobject
Returns:
Type: 
dynamic

(static) CreateArtistView(artist) → {dynamic}

Shows details on selected artist. Bio, image, and list of available albums

Parameters:
NameTypeDescription
artistobject
Returns:
Type: 
dynamic

(static) CreateHomeGroup() → {dynamic}

Returns:
Type: 
dynamic

(static) CreateItemGrid(libraryItem) → {dynamic}

Parameters:
NameTypeDescription
libraryItemobject
Returns:
Type: 
dynamic

(static) CreateMovieDetailsGroup(movie) → {dynamic}

Parameters:
NameTypeDescription
movieobject
Returns:
Type: 
dynamic

(static) CreateMovieLibraryView(libraryItem) → {dynamic}

Parameters:
NameTypeDescription
libraryItemobject
Returns:
Type: 
dynamic

(static) CreateMusicLibraryView(libraryItem) → {dynamic}

Parameters:
NameTypeDescription
libraryItemobject
Returns:
Type: 
dynamic

(static) CreatePersonView(personData) → {dynamic}

Parameters:
NameTypeDescription
personDataobject
Returns:
Type: 
dynamic

(static) CreatePlaylistView(playlist) → {dynamic}

Shows details on selected playlist. Description text, image, and list of available items

Parameters:
NameTypeDescription
playlistobject
Returns:
Type: 
dynamic

(static) CreateSearchPage() → {dynamic}

Returns:
Type: 
dynamic

(static) CreateSeasonDetailsGroup(series, season) → {dynamic}

Parameters:
NameTypeDescription
seriesobject
seasonobject
Returns:
Type: 
dynamic

(static) CreateSeasonDetailsGroupByID(seriesID, seasonID) → {dynamic}

Parameters:
NameTypeDescription
seriesIDstring
seasonIDstring
Returns:
Type: 
dynamic

(static) CreateSeriesDetailsGroup(seriesID) → {dynamic}

Parameters:
NameTypeDescription
seriesIDstring
Returns:
Type: 
dynamic

(static) CreateServerGroup() → {dynamic}

Returns:
Type: 
dynamic

(static) CreateSigninGroup(useropt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
userdynamic<optional>
""
Returns:
Type: 
dynamic

(static) CreateUserSelectGroup(usersopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
usersdynamic<optional>
[]
Returns:
Type: 
dynamic

(static) CreateVideoPlayerGroup(video_id, mediaSourceIdopt, audio_stream_idxopt, forceTranscodingopt, showIntroopt, allowResumeDialogopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
video_idstring
mediaSourceIddynamic<optional>
invalid
audio_stream_idxinteger<optional>
1
forceTranscodingboolean<optional>
false
showIntroboolean<optional>
true
allowResumeDialogboolean<optional>
true
Returns:
Type: 
dynamic

(static) DeleteFromServerList(urlToDelete) → {void}

Parameters:
NameTypeDescription
urlToDeletedynamic
Returns:
Type: 
void

(static) LoginFlow() → {dynamic}

Returns:
Type: 
dynamic

(static) SaveServerList() → {void}

Returns:
Type: 
void

(static) SendPerformanceBeacon(signalName) → {void}

Roku Performance monitoring

Parameters:
NameTypeDescription
signalNamestring
Returns:
Type: 
void

(static) playbackOptionDialog(time, meta) → {void}

Opens dialog asking user if they want to resume video or start playback over only on the home screen

Parameters:
NameTypeDescription
timelonginteger
metaobject
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SlideOutButton.html b/docs/api/module-SlideOutButton.html new file mode 100644 index 000000000..5c95354f8 --- /dev/null +++ b/docs/api/module-SlideOutButton.html @@ -0,0 +1,3 @@ +Module: SlideOutButton
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onBackgroundChanged() → {void}

Returns:
Type: 
void

(static) onFocusChanged() → {void}

Returns:
Type: 
void

(static) onHeightChanged() → {void}

Returns:
Type: 
void

(static) onHighlightChanged() → {void}

Returns:
Type: 
void

(static) onIconChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onPaddingChanged() → {void}

Returns:
Type: 
void

(static) onTextChanged() → {void}

Returns:
Type: 
void

(static) onWidthChanged() → {void}

Returns:
Type: 
void

(static) setIconSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-SongItem.html b/docs/api/module-SongItem.html new file mode 100644 index 000000000..afc156998 --- /dev/null +++ b/docs/api/module-SongItem.html @@ -0,0 +1,3 @@ +Module: SongItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Spinner.html b/docs/api/module-Spinner.html new file mode 100644 index 000000000..6e8722ee0 --- /dev/null +++ b/docs/api/module-Spinner.html @@ -0,0 +1,3 @@ +Module: Spinner
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-StandardDialog.html b/docs/api/module-StandardDialog.html new file mode 100644 index 000000000..51e04b8f6 --- /dev/null +++ b/docs/api/module-StandardDialog.html @@ -0,0 +1,3 @@ +Module: StandardDialog
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-Subtitles.html b/docs/api/module-Subtitles.html new file mode 100644 index 000000000..4b47b7377 --- /dev/null +++ b/docs/api/module-Subtitles.html @@ -0,0 +1,3 @@ +Module: Subtitles
On this page

Methods

(static) availSubtitleTrackIdx(video, sub_idx) → {integer}

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.

Parameters:
NameTypeDescription
videodynamic
sub_idxdynamic
Returns:
Type: 
integer

(static) changeSubtitleDuringPlayback(newid) → {void}

Parameters:
NameTypeDescription
newiddynamic
Returns:
Type: 
void

(static) defaultSubtitleTrack(sorted_subtitles, require_textopt) → {integer}

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

Parameters:
NameTypeAttributesDefaultDescription
sorted_subtitlesdynamic
require_textdynamic<optional>
false
Returns:
Type: 
integer

(static) defaultSubtitleTrackFromVid(video_id) → {integer}

Identify the default subtitle track for a given video id returns the server-side track index for the appriate subtitle

Parameters:
NameTypeDescription
video_iddynamic
Returns:
Type: 
integer

(static) getSubtitleLanguages() → {dynamic}

Returns:
Type: 
dynamic

(static) getSubtitleSelIdxFromSubIdx(subtitles, sub_idx) → {integer}

The subtitle index on the server differs from the index we track locally This function converts the former into the latter

Parameters:
NameTypeDescription
subtitlesdynamic
sub_idxdynamic
Returns:
Type: 
integer

(static) selectSubtitleTrack(tracks, currentopt) → {integer}

Parameters:
NameTypeAttributesDefaultDescription
tracksdynamic
currentdynamic<optional>
-1
Returns:
Type: 
integer

(static) selectSubtitleTrackDialog(tracks, currentTrackopt) → {dynamic}

Present Dialog to user to select subtitle track

Parameters:
NameTypeAttributesDefaultDescription
tracksdynamic
currentTrackdynamic<optional>
-1
Returns:
Type: 
dynamic

(static) setupSubtitle(video, subtitles, subtitle_idxopt) → {integer}

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

Parameters:
NameTypeAttributesDefaultDescription
videodynamic
subtitlesdynamic
subtitle_idxdynamic<optional>
-1
Returns:
Type: 
integer

(static) sortSubtitles(id, MediaStreams) → {dynamic}

Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top

Parameters:
NameTypeDescription
idstring
MediaStreamsdynamic
Returns:
Type: 
dynamic

(static) turnoffSubtitles() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVEpisode.html b/docs/api/module-TVEpisode.html new file mode 100644 index 000000000..8198053c5 --- /dev/null +++ b/docs/api/module-TVEpisode.html @@ -0,0 +1,3 @@ +Module: TVEpisode
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVEpisodeData.html b/docs/api/module-TVEpisodeData.html new file mode 100644 index 000000000..a009d0bbc --- /dev/null +++ b/docs/api/module-TVEpisodeData.html @@ -0,0 +1,3 @@ +Module: TVEpisodeData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVEpisodeRow.html b/docs/api/module-TVEpisodeRow.html new file mode 100644 index 000000000..2ec223a95 --- /dev/null +++ b/docs/api/module-TVEpisodeRow.html @@ -0,0 +1,3 @@ +Module: TVEpisodeRow
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) setData() → {dynamic}

Returns:
Type: 
dynamic

(static) setupRows() → {void}

Returns:
Type: 
void

(static) updateSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVEpisodeRowWithOptions.html b/docs/api/module-TVEpisodeRowWithOptions.html new file mode 100644 index 000000000..cf1917cca --- /dev/null +++ b/docs/api/module-TVEpisodeRowWithOptions.html @@ -0,0 +1,3 @@ +Module: TVEpisodeRowWithOptions
On this page

Methods

(static) SetUpAudioOptions(streams) → {void}

List of audio tracks to choose from

Parameters:
NameTypeDescription
streamsobject
Returns:
Type: 
void

(static) SetUpVideoOptions(streams) → {void}

List of video versions to choose from

Parameters:
NameTypeDescription
streamsobject
Returns:
Type: 
void

(static) audioOptionsClosed() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) rowsDoneLoading() → {void}

Returns:
Type: 
void

(static) setupRows() → {void}

Returns:
Type: 
void

(static) videoOptionsClosed() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVEpisodes.html b/docs/api/module-TVEpisodes.html new file mode 100644 index 000000000..bdc2c8560 --- /dev/null +++ b/docs/api/module-TVEpisodes.html @@ -0,0 +1,3 @@ +Module: TVEpisodes
On this page

Methods

(static) getFocusedItem() → {dynamic}

get the currently focused item

Returns:
Type: 
dynamic

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Handle navigation input from the remote and act on it

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) setExtraButtonVisibility() → {void}

Updates the visibility of the Extras button based on if this season has any extra features

Returns:
Type: 
void

(static) setSeasonLoading() → {void}

Returns:
Type: 
void

(static) updateSeason() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVListDetails.html b/docs/api/module-TVListDetails.html new file mode 100644 index 000000000..d3605863e --- /dev/null +++ b/docs/api/module-TVListDetails.html @@ -0,0 +1,3 @@ +Module: TVListDetails
On this page

Methods

(static) DisplayAudioAvailable(streams) → {void}

Adds "+N" (e.g. +1) if there is more than one audio track to choose from

Parameters:
NameTypeDescription
streamsobject
Returns:
Type: 
void

(static) DisplayVideoAvailable(streams) → {void}

Adds "+N" (e.g. +1) if there is more than one video version to choose from

Parameters:
NameTypeDescription
streamsobject
Returns:
Type: 
void

(static) SetupAudioDisplay(mediaStreams, selectedAudioStreamIndex) → {void}

Display current audio_codec and check if there is more than one audio track to choose from...

Parameters:
NameTypeDescription
mediaStreamsobject
selectedAudioStreamIndexinteger
Returns:
Type: 
void

(static) focusChanged() → {void}

Returns:
Type: 
void

(static) getEndTime() → {string}

Returns:
Type: 
string

(static) getRuntime() → {integer}

Returns:
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVListOptions.html b/docs/api/module-TVListOptions.html new file mode 100644 index 000000000..4d169d4d1 --- /dev/null +++ b/docs/api/module-TVListOptions.html @@ -0,0 +1,3 @@ +Module: TVListOptions
On this page

Methods

(static) buttonFocusChanged() → {void}

Switch menu shown when button focus changes

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) optionsSet() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVSeasonData.html b/docs/api/module-TVSeasonData.html new file mode 100644 index 000000000..31f2bb8c9 --- /dev/null +++ b/docs/api/module-TVSeasonData.html @@ -0,0 +1,3 @@ +Module: TVSeasonData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVSeasonRow.html b/docs/api/module-TVSeasonRow.html new file mode 100644 index 000000000..8139ebcd9 --- /dev/null +++ b/docs/api/module-TVSeasonRow.html @@ -0,0 +1,3 @@ +Module: TVSeasonRow
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVShowDescription.html b/docs/api/module-TVShowDescription.html new file mode 100644 index 000000000..21c2c3ebc --- /dev/null +++ b/docs/api/module-TVShowDescription.html @@ -0,0 +1,3 @@ +Module: TVShowDescription
On this page

Methods

(static) getEndTime() → {string}

Returns:
Type: 
string

(static) getHistory() → {string}

Returns:
Type: 
string

(static) getRuntime() → {integer}

Returns:
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) round(f) → {integer}

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) setFieldText(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TVShowDetails.html b/docs/api/module-TVShowDetails.html new file mode 100644 index 000000000..bc00f9e1b --- /dev/null +++ b/docs/api/module-TVShowDetails.html @@ -0,0 +1,3 @@ +Module: TVShowDetails
On this page

Methods

(static) getEndTime() → {string}

Returns:
Type: 
string

(static) getHistory() → {string}

Returns:
Type: 
string

(static) getRuntime() → {integer}

Returns:
Type: 
integer

(static) init() → {void}

Returns:
Type: 
void

(static) itemContentChanged() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onShuffleEpisodeDataLoaded() → {void}

Returns:
Type: 
void

(static) round(f) → {integer}

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) setFieldText(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-TextSizeTask.html b/docs/api/module-TextSizeTask.html new file mode 100644 index 000000000..5d0ec98d8 --- /dev/null +++ b/docs/api/module-TextSizeTask.html @@ -0,0 +1,3 @@ +Module: TextSizeTask
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-UserData.html b/docs/api/module-UserData.html new file mode 100644 index 000000000..bb9c9720e --- /dev/null +++ b/docs/api/module-UserData.html @@ -0,0 +1,3 @@ +Module: UserData
On this page

Methods

(static) getPreference(key) → {dynamic}

Parameters:
NameTypeDescription
keystring
Returns:
Type: 
dynamic

(static) loadFromJSON(json) → {void}

Parameters:
NameTypeDescription
jsondynamic
Returns:
Type: 
void

(static) loadFromRegistry(id) → {void}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
void

(static) removeFromRegistry() → {void}

Returns:
Type: 
void

(static) saveToRegistry() → {void}

Returns:
Type: 
void

(static) setDataFromJSON() → {void}

Returns:
Type: 
void

(static) setPreference(key, value) → {dynamic}

Parameters:
NameTypeDescription
keystring
valuestring
Returns:
Type: 
dynamic

(static) setServer(hostname) → {void}

Parameters:
NameTypeDescription
hostnamestring
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-UserItem.html b/docs/api/module-UserItem.html new file mode 100644 index 000000000..f0e7e4afc --- /dev/null +++ b/docs/api/module-UserItem.html @@ -0,0 +1,3 @@ +Module: UserItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-UserLibrary.html b/docs/api/module-UserLibrary.html new file mode 100644 index 000000000..d8efcccbf --- /dev/null +++ b/docs/api/module-UserLibrary.html @@ -0,0 +1,3 @@ +Module: UserLibrary
On this page

Methods

(static) MarkItemFavorite(id) → {dynamic}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) MarkItemWatched(id) → {void}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
void

(static) UnmarkItemFavorite(id) → {dynamic}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic

(static) UnmarkItemWatched(id) → {dynamic}

Parameters:
NameTypeDescription
idstring
Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-UserRow.html b/docs/api/module-UserRow.html new file mode 100644 index 000000000..11f64102b --- /dev/null +++ b/docs/api/module-UserRow.html @@ -0,0 +1,3 @@ +Module: UserRow
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) setData() → {dynamic}

Returns:
Type: 
dynamic

(static) setUser() → {void}

Returns:
Type: 
void

(static) updateSize() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-UserSelect.html b/docs/api/module-UserSelect.html new file mode 100644 index 000000000..c5369ab42 --- /dev/null +++ b/docs/api/module-UserSelect.html @@ -0,0 +1,3 @@ +Module: UserSelect
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-VideoData.html b/docs/api/module-VideoData.html new file mode 100644 index 000000000..16b006b01 --- /dev/null +++ b/docs/api/module-VideoData.html @@ -0,0 +1,3 @@ +Module: VideoData
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-VideoPlayer.html b/docs/api/module-VideoPlayer.html new file mode 100644 index 000000000..93e310269 --- /dev/null +++ b/docs/api/module-VideoPlayer.html @@ -0,0 +1,3 @@ +Module: VideoPlayer
On this page

Methods

(static) AddVideoContent(video, mediaSourceId, audio_stream_idxopt, subtitle_idxopt, playbackPositionopt, forceTranscodingopt, showIntroopt, allowResumeDialogopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
videoobject
mediaSourceIddynamic
audio_stream_idxinteger<optional>
1
subtitle_idxinteger<optional>
-1
playbackPositioninteger<optional>
-1
forceTranscodingboolean<optional>
false
showIntroboolean<optional>
true
allowResumeDialogboolean<optional>
true
Returns:
Type: 
void

(static) GetPlaybackInfo() → {dynamic}

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.

Returns:
Type: 
dynamic

(static) GetTranscodingStats(deviceSession) → {dynamic}

Parameters:
NameTypeDescription
deviceSessiondynamic
Returns:
Type: 
dynamic

(static) PlayIntroVideo(video_id, audio_stream_idx) → {boolean}

Parameters:
NameTypeDescription
video_iddynamic
audio_stream_idxdynamic
Returns:
Type: 
boolean

(static) VideoPlayer(id, mediaSourceIdopt, audio_stream_idxopt, subtitle_idxopt, forceTranscodingopt, showIntroopt, allowResumeDialogopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring
mediaSourceIddynamic<optional>
invalid
audio_stream_idxinteger<optional>
1
subtitle_idxinteger<optional>
-1
forceTranscodingboolean<optional>
false
showIntroboolean<optional>
true
allowResumeDialogboolean<optional>
true
Returns:
Type: 
dynamic

(static) autoPlayNextEpisode(videoID, showID) → {void}

Parameters:
NameTypeDescription
videoIDstring
showIDstring
Returns:
Type: 
void

(static) directPlaySupported(meta) → {boolean}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
boolean

(static) getAudioFormat(meta) → {string}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
string

(static) getAudioInfo(meta) → {object}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
object

(static) getContainerType(meta) → {string}

Parameters:
NameTypeDescription
metaobject
Returns:
Type: 
string

(static) getDisplayBitrate(bitrate) → {dynamic}

Parameters:
NameTypeDescription
bitratedynamic
Returns:
Type: 
dynamic

(static) getTranscodeReasons(url) → {object}

Extract array of Transcode Reasons from the content URL

Parameters:
NameTypeDescription
urlstring
Returns:
  • Array of Strings
Type: 
object

(static) havePlaybackInfo() → {dynamic}

Returns:
Type: 
dynamic

(static) startPlayBackOver(time) → {integer}

Opens dialog asking user if they want to resume video or start playback over only on the home screen

Parameters:
NameTypeDescription
timelonginteger
Returns:
Type: 
integer
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-VideoPlayerView.html b/docs/api/module-VideoPlayerView.html new file mode 100644 index 000000000..21ce5aa54 --- /dev/null +++ b/docs/api/module-VideoPlayerView.html @@ -0,0 +1,3 @@ +Module: VideoPlayerView
On this page

Methods

(static) ReportPlayback(stateopt) → {void}

Report playback to server

Parameters:
NameTypeAttributesDefaultDescription
statestring<optional>
"update"
Returns:
Type: 
void

(static) bufferCheck(msg) → {void}

Check the the buffering has not hung

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) checkTimeToDisplayNextEpisode() → {void}

Checks if we need to display the Next Episode button

Returns:
Type: 
void

(static) getCurrentChapterIndex() → {integer}

getCurrentChapterIndex: Finds current chapter index

Returns:
  • indicating index of current chapter within chapter data or 0 if chapter lookup fails
Type: 
integer

(static) handleChapterListAction() → {void}

handleChapterListAction: Handles action to show chapter list

Returns:
Type: 
void

(static) handleChapterSkipAction(action) → {void}

handleChapterSkipAction: Handles user command to skip chapters in playing video

Parameters:
NameTypeDescription
actionstring
Returns:
Type: 
void

(static) handleHideAction(resume) → {void}

handleHideAction: Handles action to hide OSD menu

Parameters:
NameTypeDescription
resumeboolean

controls whether or not to resume video playback when sub is called

Returns:
Type: 
void

(static) handleShowSubtitleMenuAction() → {void}

handleShowSubtitleMenuAction: Handles action to show subtitle selection menu

Returns:
Type: 
void

(static) handleShowVideoInfoPopupAction() → {void}

handleShowVideoInfoPopupAction: Handles action to show video info popup

Returns:
Type: 
void

(static) handleVideoPlayPauseAction() → {void}

handleVideoPlayPauseAction: Handles action to either play or pause the video content

Returns:
Type: 
void

(static) hideNextEpisodeButton() → {void}

Runs hide Next Episode button animation and sets focus back to video

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) loadCaption() → {void}

Set caption url to server subtitle track

Returns:
Type: 
void

(static) onAllowCaptionsChange() → {void}

Only setup caption items if captions are allowed

Returns:
Type: 
void

(static) onContentChange() → {void}

Event handler for when video content field changes

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onNextEpisodeDataLoaded() → {void}

Returns:
Type: 
void

(static) onOSDAction() → {void}

onOSDAction: Process action events from OSD to their respective handlers

Returns:
Type: 
void

(static) onPlaybackErrorButtonSelected(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onPlaybackErrorDialogClosed(msg) → {void}

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onPositionChanged() → {void}

When Video Player state changes

Returns:
Type: 
void

(static) onState(msg) → {void}

When Video Player state changes

Parameters:
NameTypeDescription
msgdynamic
Returns:
Type: 
void

(static) onSubtitleChange() → {void}

Event handler for when selectedSubtitle changes

Returns:
Type: 
void

(static) onVideoContentLoaded() → {void}

Returns:
Type: 
void

(static) populateChapterMenu() → {void}

populateChapterMenu: ' Parse chapter data from API and appeand to chapter list menu

Returns:
Type: 
void

(static) showNextEpisodeButton() → {void}

Runs Next Episode button animation and sets focus to button

Returns:
Type: 
void

(static) showPlaybackErrorDialog(errorMessage) → {void}

Parameters:
NameTypeDescription
errorMessagestring
Returns:
Type: 
void

(static) stateAllowsOSD() → {boolean}

stateAllowsOSD: Check if current video state allows showing the OSD

Returns:
  • indicating if video state allows the OSD to show
Type: 
boolean

(static) toggleCaption() → {void}

Toggles visibility of custom subtitles and sets captionTask's player state

Returns:
Type: 
void

(static) updateCaption() → {void}

Removes old subtitle lines and adds new subtitle lines

Returns:
Type: 
void

(static) updateCount() → {void}

Update count down text

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-VideoTrackListItem.html b/docs/api/module-VideoTrackListItem.html new file mode 100644 index 000000000..66d23390d --- /dev/null +++ b/docs/api/module-VideoTrackListItem.html @@ -0,0 +1,3 @@ +Module: VideoTrackListItem
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-ViewCreator.html b/docs/api/module-ViewCreator.html new file mode 100644 index 000000000..83bcfd44c --- /dev/null +++ b/docs/api/module-ViewCreator.html @@ -0,0 +1,3 @@ +Module: ViewCreator
On this page

Methods

(static) CreateAudioPlayerView() → {void}

Play Audio

Returns:
Type: 
void

(static) CreateVideoPlayerView() → {void}

Play Video

Returns:
Type: 
void

(static) availSubtitleTrackIdx(tracknameToFind) → {integer}

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.

Parameters:
NameTypeDescription
tracknameToFindstring
Returns:
Type: 
integer

(static) onPlaybackInfoLoaded() → {void}

The playback info task has returned data

Returns:
Type: 
void

(static) onSelectPlaybackInfoPressed() → {void}

User requested playback info

Returns:
Type: 
void

(static) onSelectSubtitlePressed() → {void}

User requested subtitle selection popup

Returns:
Type: 
void

(static) onSelectionMade() → {void}

User has selected something from the radioDialog popup

Returns:
Type: 
void

(static) onStateChange() → {void}

Playback state change event handlers

Returns:
Type: 
void

(static) processSubtitleSelection() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-WhatsNewDialog.html b/docs/api/module-WhatsNewDialog.html new file mode 100644 index 000000000..d19917878 --- /dev/null +++ b/docs/api/module-WhatsNewDialog.html @@ -0,0 +1,3 @@ +Module: WhatsNewDialog
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-baserequest.html b/docs/api/module-baserequest.html new file mode 100644 index 000000000..cd70c00a6 --- /dev/null +++ b/docs/api/module-baserequest.html @@ -0,0 +1,3 @@ +Module: baserequest
On this page

Methods

(static) APIRequest(url, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
urlstring
paramsobject<optional>
{}
Returns:
Type: 
dynamic

(static) authRequest(req) → {object}

Takes and returns a roUrlTransfer object after adding a Jellyfin "Authorization" header

Parameters:
NameTypeDescription
reqobject
Returns:
Type: 
object

(static) buildAuthHeader() → {string}

Returns a string containing the "Authorization" header payload

Returns:
Type: 
string

(static) buildParams(paramsopt) → {string}

Functions for making requests to the API

Parameters:
NameTypeAttributesDefaultDescription
paramsobject<optional>
{}
Returns:
Type: 
string

(static) buildURL(path, paramsopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
pathstring
paramsobject<optional>
{}
Returns:
Type: 
dynamic

(static) deleteVoid(req) → {dynamic}

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
dynamic

(static) getJson(req) → {dynamic}

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
dynamic

(static) getString(req) → {dynamic}

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
dynamic

(static) getVoid(req) → {boolean}

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
boolean

(static) get_url() → {dynamic}

Returns:
Type: 
dynamic

(static) headVoid(req) → {boolean}

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
boolean

(static) postJson(req, dataopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
reqdynamic
datastring<optional>
""
Returns:
Type: 
dynamic

(static) postString(req, dataopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
reqdynamic
datastring<optional>
""
Returns:
Type: 
dynamic

(static) postVoid(req, dataopt) → {boolean}

Parameters:
NameTypeAttributesDefaultDescription
reqdynamic
datastring<optional>
""
Returns:
Type: 
boolean

(static) setCertificateAuthority(request) → {void}

sets the certificate authority by file path on the passed node

Parameters:
NameTypeDescription
requestobject
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-captionTask.html b/docs/api/module-captionTask.html new file mode 100644 index 000000000..e6fc9df69 --- /dev/null +++ b/docs/api/module-captionTask.html @@ -0,0 +1,3 @@ +Module: captionTask
On this page

Methods

(static) fetchCaption() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isTime(text) → {dynamic}

Parameters:
NameTypeDescription
textdynamic
Returns:
Type: 
dynamic

(static) newLayoutGroup(labels) → {dynamic}

Parameters:
NameTypeDescription
labelsdynamic
Returns:
Type: 
dynamic

(static) newRect(lg) → {dynamic}

Parameters:
NameTypeDescription
lgdynamic
Returns:
Type: 
dynamic

(static) newlabel(txt) → {dynamic}

Parameters:
NameTypeDescription
txtdynamic
Returns:
Type: 
dynamic

(static) parseVTT(lines) → {dynamic}

Parameters:
NameTypeDescription
linesdynamic
Returns:
Type: 
dynamic

(static) setFont() → {void}

Returns:
Type: 
void

(static) toMs(t) → {dynamic}

Parameters:
NameTypeDescription
tdynamic
Returns:
Type: 
dynamic

(static) updateCaption() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-conditional.html b/docs/api/module-conditional.html new file mode 100644 index 000000000..a1673ad0a --- /dev/null +++ b/docs/api/module-conditional.html @@ -0,0 +1,3 @@ +Module: conditional
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-config.html b/docs/api/module-config.html new file mode 100644 index 000000000..adafe9b68 --- /dev/null +++ b/docs/api/module-config.html @@ -0,0 +1,3 @@ +Module: config
On this page

Methods

(static) GetConfigTree() → {dynamic}

Read config tree from json config file and return

Returns:
Type: 
dynamic

(static) RegistryReadAll(section) → {dynamic}

Return all data found inside a registry section

Parameters:
NameTypeDescription
sectionstring
Returns:
Type: 
dynamic

(static) findConfigTreeKey(key, tree) → {dynamic}

Recursivly search the config tree for entry with settingname equal to key

Parameters:
NameTypeDescription
keystring
treedynamic
Returns:
Type: 
dynamic

(static) getRegistrySections() → {object}

Return an array of all the registry section keys

Returns:
Type: 
object

(static) getSavedUsers() → {object}

Returns an array of saved users from the registry that belong to the active server

Returns:
Type: 
object

(static) get_setting(key, defaultValueopt) → {dynamic}

"Jellyfin" registry accessors for the default global settings

Parameters:
NameTypeAttributesDefaultDescription
keydynamic
defaultValuedynamic<optional>
invalid
Returns:
Type: 
dynamic

(static) get_user_setting(key) → {dynamic}

User registry accessors for the currently active user

Parameters:
NameTypeDescription
keystring
Returns:
Type: 
dynamic

(static) registry_delete(key, sectionopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
keydynamic
sectiondynamic<optional>
invalid
Returns:
Type: 
void

(static) registry_read(key, sectionopt) → {dynamic}

Generic registry accessors

Parameters:
NameTypeAttributesDefaultDescription
keydynamic
sectiondynamic<optional>
invalid
Returns:
Type: 
dynamic

(static) registry_write(key, value, sectionopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
keydynamic
valuedynamic
sectiondynamic<optional>
invalid
Returns:
Type: 
void

(static) set_setting(key, value) → {void}

Parameters:
NameTypeDescription
keydynamic
valuedynamic
Returns:
Type: 
void

(static) set_user_setting(key, value) → {void}

Parameters:
NameTypeDescription
keystring
valuedynamic
Returns:
Type: 
void

(static) unset_setting(key) → {void}

Parameters:
NameTypeDescription
keydynamic
Returns:
Type: 
void

(static) unset_user_setting(key) → {void}

Parameters:
NameTypeDescription
keystring
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-deviceCapabilities.html b/docs/api/module-deviceCapabilities.html new file mode 100644 index 000000000..d4c9705a4 --- /dev/null +++ b/docs/api/module-deviceCapabilities.html @@ -0,0 +1,3 @@ +Module: deviceCapabilities
On this page

Methods

(static) GetBitRateLimit(codec) → {object}

Parameters:
NameTypeDescription
codecstring
Returns:
Type: 
object

(static) GetDirectPlayProfiles() → {object}

Returns:
Type: 
object

(static) getCodecProfiles() → {object}

Returns:
Type: 
object

(static) getContainerProfiles() → {object}

Returns:
Type: 
object

(static) getDeviceCapabilities() → {object}

Returns the Device Capabilities for Roku. Also prints out the device profile for debugging

Returns:
Type: 
object

(static) getDeviceProfile() → {object}

Returns:
Type: 
object

(static) getMaxHeightArray() → {object}

Returns:
Type: 
object

(static) getMaxWidthArray() → {object}

Returns:
Type: 
object

(static) getSubtitleProfiles() → {object}

Returns:
Type: 
object

(static) getTranscodingProfiles() → {object}

Returns:
Type: 
object

(static) printDeviceProfile(profile) → {void}

Print out the deviceProfile for debugging

Parameters:
NameTypeDescription
profileobject
Returns:
Type: 
void

(static) removeDecimals(value) → {string}

Remove all decimals from a string

Parameters:
NameTypeDescription
valuestring
Returns:
Type: 
string

(static) setPreferredCodec(codecString, preferredCodec) → {string}

Takes and returns a comma delimited string of codecs. Moves the preferred codec to the front of the string

Parameters:
NameTypeDescription
codecStringstring
preferredCodecstring
Returns:
Type: 
string

(static) updateProfileArray(profileArray, videoCodec, videoProfile, profileLevelopt) → {object}

Recieves and returns an assArray of supported profiles and levels for each video codec

Parameters:
NameTypeAttributesDefaultDescription
profileArrayobject
videoCodecstring
videoProfilestring
profileLevelstring<optional>
""
Returns:
Type: 
object
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-globals.html b/docs/api/module-globals.html new file mode 100644 index 000000000..21880914f --- /dev/null +++ b/docs/api/module-globals.html @@ -0,0 +1,3 @@ +Module: globals
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-homeRowItemSizes_.html b/docs/api/module-homeRowItemSizes_.html new file mode 100644 index 000000000..98f8063b7 --- /dev/null +++ b/docs/api/module-homeRowItemSizes_.html @@ -0,0 +1,3 @@ +Module: homeRowItemSizes
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-migrations.html b/docs/api/module-migrations.html new file mode 100644 index 000000000..5ef17c768 --- /dev/null +++ b/docs/api/module-migrations.html @@ -0,0 +1,3 @@ +Module: migrations
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-misc.html b/docs/api/module-misc.html new file mode 100644 index 000000000..90edcdbb9 --- /dev/null +++ b/docs/api/module-misc.html @@ -0,0 +1,3 @@ +Module: misc
On this page

Methods

(static) AssocArrayEqual(Array1, Array2) → {boolean}

Parameters:
NameTypeDescription
Array1object
Array2object
Returns:
Type: 
boolean

(static) arrayHasValue(arr, value) → {boolean}

Check if a specific value is inside of an array

Parameters:
NameTypeDescription
arrobject
valuedynamic
Returns:
Type: 
boolean

(static) div_ceiling(a, b) → {integer}

Parameters:
NameTypeDescription
ainteger
binteger
Returns:
Type: 
integer

(static) findNodeBySubtype(node, subtype) → {dynamic}

Parameters:
NameTypeDescription
nodedynamic
subtypedynamic
Returns:
Type: 
dynamic

(static) formatTime(time) → {string}

Format time as 12 or 24 hour format based on system clock setting

Parameters:
NameTypeDescription
timedynamic
Returns:
Type: 
string

(static) getButton(msg, subnodeopt) → {object}

Parameters:
NameTypeAttributesDefaultDescription
msgdynamic
subnodestring<optional>
"buttons"
Returns:
Type: 
object

(static) getMinutes(ticks) → {integer}

Converts ticks to minutes

Parameters:
NameTypeDescription
ticksdynamic
Returns:
Type: 
integer

(static) getMsgPicker(msg, subnodeopt) → {object}

Parameters:
NameTypeAttributesDefaultDescription
msgdynamic
subnodestring<optional>
""
Returns:
Type: 
object

(static) get_dialog_result(dialog, port) → {dynamic}

Returns the item selected or -1 on backpress or other unhandled closure of dialog.

Parameters:
NameTypeDescription
dialogdynamic
portdynamic
Returns:
Type: 
dynamic

(static) inArray(haystack, needle) → {boolean}

Search string array for search value. Return if it's found

Parameters:
NameTypeDescription
haystackdynamic
needledynamic
Returns:
Type: 
boolean

(static) inferServerUrl(url) → {string}

take an incomplete url string and use it to make educated guesses about the complete url. then tests these guesses to see if it can find a jf server returns the url of the server it found, or an empty string

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
string

(static) isJellyfinServer(systemInfo) → {boolean}

accepts the raw json string of /system/info/public and returns a boolean indicating if ProductName is "Jellyfin Server"

Parameters:
NameTypeDescription
systemInfoobject
Returns:
Type: 
boolean

(static) isLocalhost(url) → {boolean}

Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
boolean

(static) isNodeEvent(msg, field) → {boolean}

Parameters:
NameTypeDescription
msgdynamic
fieldstring
Returns:
Type: 
boolean

(static) isValid(input) → {boolean}

Returns whether or not passed value is valid

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
boolean

(static) isValidAndNotEmpty(input) → {boolean}

Returns whether or not passed value is valid and not empty Accepts a string, or any countable type (arrays and lists)

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
boolean

(static) lastFocusedChild(obj) → {object}

Parameters:
NameTypeDescription
objobject
Returns:
Type: 
object

(static) leftPad(base, fill, length) → {string}

Parameters:
NameTypeDescription
basestring
fillstring
lengthinteger
Returns:
Type: 
string

(static) message_dialog(messageopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
messagestring<optional>
""
Returns:
Type: 
dynamic

(static) option_dialog(options, messageopt, defaultSelectionopt) → {integer}

Parameters:
NameTypeAttributesDefaultDescription
optionsdynamic
messagedynamic<optional>
""
defaultSelectiondynamic<optional>
0
Returns:
Type: 
integer

(static) parseUrl(url) → {object}

Returns an array from a url = [ url, proto, host, port, subdir+params ] If port or subdir are not found, an empty string will be added to the array Proto must be declared or array will be empty

Parameters:
NameTypeDescription
urlstring
Returns:
Type: 
object

(static) roundNumber(f) → {integer}

Rounds number to nearest integer

Parameters:
NameTypeDescription
ffloat
Returns:
Type: 
integer

(static) secondsToHuman(totalSeconds, addLeadingMinuteZero) → {string}

Parameters:
NameTypeDescription
totalSecondsinteger
addLeadingMinuteZeroboolean
Returns:
Type: 
string

(static) setFieldTextValue(field, value) → {void}

Parameters:
NameTypeDescription
fielddynamic
valuedynamic
Returns:
Type: 
void

(static) show_dialog(message, optionsopt, defaultSelectionopt) → {integer}

Parameters:
NameTypeAttributesDefaultDescription
messagestring
optionsdynamic<optional>
[]
defaultSelectiondynamic<optional>
0
Returns:
Type: 
integer

(static) shuffleArray(array) → {object}

Takes an array of data, shuffles the order, then returns the array uses the Fisher-Yates shuffling algorithm

Parameters:
NameTypeDescription
arrayobject
Returns:
Type: 
object

(static) startLoadingSpinner(disableRemoteopt) → {void}

startLoadingSpinner: Start a loading spinner and attach it to the main JFScene. Displays an invisible ProgressDialog node by default to disable keypresses while loading.

Parameters:
NameTypeAttributesDefaultDescription
disableRemoteboolean<optional>
true
Returns:
Type: 
void

(static) stopLoadingSpinner() → {void}

Returns:
Type: 
void

(static) ticksToHuman(ticks) → {string}

Parameters:
NameTypeDescription
tickslonginteger
Returns:
Type: 
string

(static) toString(input) → {string}

Parameters:
NameTypeDescription
inputdynamic
Returns:
Type: 
string

(static) urlCandidates(input) → {dynamic}

this is the "educated guess" logic for inferServerUrl that generates a list of complete url's as candidates for the tests in inferServerUrl. takes an incomplete url as an arg and returns a list of extrapolated full urls.

Parameters:
NameTypeDescription
inputstring
Returns:
Type: 
dynamic

(static) versionChecker(versionToCheck, minVersionAccepted) → {dynamic}

Returns whether or not a version number (e.g. 10.7.7) is greater or equal to some minimum version allowed (e.g. 10.8.0)

Parameters:
NameTypeDescription
versionToCheckstring
minVersionAcceptedstring
Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-quickplay.html b/docs/api/module-quickplay.html new file mode 100644 index 000000000..4c6322c99 --- /dev/null +++ b/docs/api/module-quickplay.html @@ -0,0 +1,3 @@ +Module: quickplay
On this page

Methods

(static) album(itemNode) → {void}

A music album. Play the entire album starting with track 1.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) artist(itemNode) → {void}

A music artist. Shuffle play all songs by artist.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) audio(itemNode) → {void}

A single audio file.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) boxset(itemNode) → {void}

A boxset. Play all items inside.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) collectionFolder(itemNode) → {void}

Quick Play A CollectionFolder. Shuffle play the items inside with some differences based on collectionType.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) folder(itemNode) → {void}

Quick Play A folder. Shuffle play all items found

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) multipleSeries(itemNodes) → {void}

More than one TV Show Series. Shuffle play all watched episodes

Parameters:
NameTypeDescription
itemNodesobject
Returns:
Type: 
void

(static) musicVideo(itemNode) → {void}

A single music video file.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) person(itemNode) → {void}

Quick Play A Person. Shuffle play all videos found

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) photo(itemNode) → {void}

A single photo.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) photoAlbum(itemNode) → {void}

A photo album.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) playlist(itemNode) → {void}

Quick Play A Playlist. Play the first unwatched episode. If none, play the whole season starting with episode 1.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) program(itemNode) → {void}

Quick Play A Live Program

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) pushToQueue(queueArray, shufflePlayopt) → {void}

Takes an array of items and adds to global queue. Also shuffles the playlist if asked

Parameters:
NameTypeAttributesDefaultDescription
queueArrayobject
shufflePlayboolean<optional>
false
Returns:
Type: 
void

(static) season(itemNode) → {void}

A TV Show Season. Play the first unwatched episode. If none, play the whole season starting with episode 1.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) series(itemNode) → {void}

A TV Show Series. Play the first unwatched episode. If none, shuffle play the whole series.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) tvChannel(itemNode) → {void}

Quick Play A TVChannel

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) userView(itemNode) → {void}

Quick Play A UserView. Play logic depends on "collectionType".

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) video(itemNode) → {void}

A single video file.

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void

(static) videoContainer(itemNode) → {void}

A container with some kind of videos inside of it

Parameters:
NameTypeDescription
itemNodeobject
Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-schedule.html b/docs/api/module-schedule.html new file mode 100644 index 000000000..a0f416ca7 --- /dev/null +++ b/docs/api/module-schedule.html @@ -0,0 +1,3 @@ +Module: schedule
On this page

Methods

(static) channelFilterSet() → {void}

Returns:
Type: 
void

(static) channelsearchTermSet() → {void}

Voice Search set

Returns:
Type: 
void

(static) focusProgramDetails(setFocused) → {void}

Move the TV Guide Grid down or up depending whether details are selected

Parameters:
NameTypeDescription
setFocuseddynamic
Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) onChannelsLoaded() → {void}

Initial list of channels loaded

Returns:
Type: 
void

(static) onGridScrolled() → {void}

As user scrolls grid, check if more data requries to be loaded

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onProgramDetailsLoaded() → {void}

Update the Program Details with full information

Returns:
Type: 
void

(static) onProgramFocused() → {void}

Returns:
Type: 
void

(static) onProgramSelected() → {void}

Returns:
Type: 
void

(static) onRecordChannelSelected() → {void}

Handle user selecting "Record Channel" from Program Details

Returns:
Type: 
void

(static) onRecordOperationDone() → {void}

Returns:
Type: 
void

(static) onRecordSeriesChannelSelected() → {void}

Handle user selecting "Record Series" from Program Details

Returns:
Type: 
void

(static) onScheduleLoaded() → {void}

When LoadScheduleTask completes (initial or more data) and we have a schedule to display

Returns:
Type: 
void

(static) onWatchChannelSelected() → {void}

Handle user selecting "Watch Channel" from Program Details

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-section.html b/docs/api/module-section.html new file mode 100644 index 000000000..ba2882ee6 --- /dev/null +++ b/docs/api/module-section.html @@ -0,0 +1,3 @@ +Module: section
On this page

Methods

(static) init() → {void}

Returns:
Type: 
void

(static) onFocusChange() → {void}

Returns:
Type: 
void

(static) onIDChange() → {void}

Returns:
Type: 
void

(static) onTranslationChange() → {void}

Returns:
Type: 
void

(static) scrollDownToOnDeck() → {void}

Returns:
Type: 
void

(static) scrollOffBottom() → {void}

Returns:
Type: 
void

(static) scrollOffOnDeck() → {void}

Returns:
Type: 
void

(static) scrollOffTop() → {void}

Returns:
Type: 
void

(static) scrollUpToOnDeck() → {void}

Returns:
Type: 
void

(static) showFromBottom() → {void}

Returns:
Type: 
void

(static) showFromTop() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-sectionScroller.html b/docs/api/module-sectionScroller.html new file mode 100644 index 000000000..f8816a302 --- /dev/null +++ b/docs/api/module-sectionScroller.html @@ -0,0 +1,3 @@ +Module: sectionScroller
On this page
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-settings.html b/docs/api/module-settings.html new file mode 100644 index 000000000..9f636b581 --- /dev/null +++ b/docs/api/module-settings.html @@ -0,0 +1,3 @@ +Module: settings
On this page

Methods

(static) LoadMenu(configSection) → {void}

Parameters:
NameTypeDescription
configSectiondynamic
Returns:
Type: 
void

(static) OnScreenHidden() → {void}

JFScreen hook that gets ran as needed. Assumes settings were changed and they affect the device profile. Posts a new device profile to the server using the task thread

Returns:
Type: 
void

(static) boolSettingChanged() → {void}

Returns:
Type: 
void

(static) init() → {void}

Returns:
Type: 
void

(static) isFormInFocus() → {boolean}

Returns true if any of the data entry forms are in focus

Returns:
Type: 
boolean

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onKeyGridEscape() → {void}

Returns:
Type: 
void

(static) onKeyGridSubmit() → {void}

Returns:
Type: 
void

(static) postFinished() → {void}

Triggered by m.postTask after completing a post. Empty the task data when finished.

Returns:
Type: 
void

(static) radioSettingChanged() → {void}

Returns:
Type: 
void

(static) settingFocused() → {void}

Returns:
Type: 
void

(static) settingSelected() → {void}

Returns:
Type: 
void
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/module-userauth.html b/docs/api/module-userauth.html new file mode 100644 index 000000000..2a78566b5 --- /dev/null +++ b/docs/api/module-userauth.html @@ -0,0 +1,3 @@ +Module: userauth
On this page

Methods

(static) AboutMe(idopt) → {dynamic}

Parameters:
NameTypeAttributesDefaultDescription
idstring<optional>
""
Returns:
Type: 
dynamic

(static) AuthenticateViaQuickConnect(secret) → {dynamic}

Parameters:
NameTypeDescription
secretdynamic
Returns:
Type: 
dynamic

(static) AvailableUsers() → {dynamic}

Returns:
Type: 
dynamic

(static) GetPublicUsers() → {dynamic}

Returns:
Type: 
dynamic

(static) LoadUserAbilities() → {void}

Returns:
Type: 
void

(static) ServerInfo() → {dynamic}

Returns:
Type: 
dynamic

(static) SignOut(deleteSavedEntryopt) → {void}

Parameters:
NameTypeAttributesDefaultDescription
deleteSavedEntryboolean<optional>
true
Returns:
Type: 
void

(static) checkQuickConnect(secret) → {dynamic}

Parameters:
NameTypeDescription
secretdynamic
Returns:
Type: 
dynamic

(static) get_token(user, password) → {dynamic}

Parameters:
NameTypeDescription
userstring
passwordstring
Returns:
Type: 
dynamic

(static) initQuickConnect() → {dynamic}

Returns:
Type: 
dynamic
jellyfin-roku Code Documentation
\ No newline at end of file diff --git a/docs/api/scripts/core.js b/docs/api/scripts/core.js new file mode 100644 index 000000000..6344a3d9f --- /dev/null +++ b/docs/api/scripts/core.js @@ -0,0 +1,702 @@ +/* global document */ +var accordionLocalStorageKey = 'accordion-id'; +var themeLocalStorageKey = 'theme'; +var fontSizeLocalStorageKey = 'font-size'; +var html = document.querySelector('html'); + +var MAX_FONT_SIZE = 30; +var MIN_FONT_SIZE = 10; + +// eslint-disable-next-line no-undef +var localStorage = window.localStorage; + +function getTheme() { + var theme = localStorage.getItem(themeLocalStorageKey); + + if (theme) return theme; + + theme = document.body.getAttribute('data-theme'); + + switch (theme) { + case 'dark': + case 'light': + return theme; + case 'fallback-dark': + if ( + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme)').matches && + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme: light)').matches + ) { + return 'light'; + } + + return 'dark'; + + case 'fallback-light': + if ( + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme)').matches && + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme: dark)').matches + ) { + return 'dark'; + } + + return 'light'; + + default: + return 'dark'; + } +} + +function localUpdateTheme(theme) { + var body = document.body; + var svgUse = document.querySelectorAll('.theme-svg-use'); + var iconID = theme === 'dark' ? '#light-theme-icon' : '#dark-theme-icon'; + + body.setAttribute('data-theme', theme); + body.classList.remove('dark', 'light'); + body.classList.add(theme); + + svgUse.forEach(function (svg) { + svg.setAttribute('xlink:href', iconID); + }); +} + +function updateTheme(theme) { + localUpdateTheme(theme); + localStorage.setItem(themeLocalStorageKey, theme); +} + +function toggleTheme() { + var body = document.body; + var theme = body.getAttribute('data-theme'); + + var newTheme = theme === 'dark' ? 'light' : 'dark'; + + updateTheme(newTheme); +} + +(function () { + var theme = getTheme(); + + updateTheme(theme); +})(); + +/** + * Function to set accordion id to localStorage. + * @param {string} id Accordion id + */ +function setAccordionIdToLocalStorage(id) { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + ids[id] = id; + localStorage.setItem(accordionLocalStorageKey, JSON.stringify(ids)); +} + +/** + * Function to remove accordion id from localStorage. + * @param {string} id Accordion id + */ +function removeAccordionIdFromLocalStorage(id) { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + delete ids[id]; + localStorage.setItem(accordionLocalStorageKey, JSON.stringify(ids)); +} + +/** + * Function to get all accordion ids from localStorage. + * + * @returns {object} + */ +function getAccordionIdsFromLocalStorage() { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + return ids || {}; +} + +function toggleAccordion(element) { + var currentNode = element; + var isCollapsed = currentNode.getAttribute('data-isopen') === 'false'; + + if (isCollapsed) { + currentNode.setAttribute('data-isopen', 'true'); + setAccordionIdToLocalStorage(currentNode.id); + } else { + currentNode.setAttribute('data-isopen', 'false'); + removeAccordionIdFromLocalStorage(currentNode.id); + } +} + +function initAccordion() { + if ( + localStorage.getItem(accordionLocalStorageKey) === undefined || + localStorage.getItem(accordionLocalStorageKey) === null + ) { + localStorage.setItem(accordionLocalStorageKey, '{}'); + } + var allAccordion = document.querySelectorAll('.sidebar-section-title'); + var ids = getAccordionIdsFromLocalStorage(); + + allAccordion.forEach(function (item) { + item.addEventListener('click', function () { + toggleAccordion(item); + }); + if (item.id in ids) { + toggleAccordion(item); + } + }); +} + +function isSourcePage() { + return Boolean(document.querySelector('#source-page')); +} + +function bringElementIntoView(element, updateHistory = true) { + // If element is null then we are not going further + if (!element) { + return; + } + + /** + * tocbotInstance is defined in layout.tmpl + * It is defined when we are initializing tocbot. + * + */ + // eslint-disable-next-line no-undef + if (tocbotInstance) { + setTimeout( + // eslint-disable-next-line no-undef + () => tocbotInstance.updateTocListActiveElement(element), + 60 + ); + } + var navbar = document.querySelector('.navbar-container'); + var body = document.querySelector('.main-content'); + var elementTop = element.getBoundingClientRect().top; + + var offset = 16; + + if (navbar) { + offset += navbar.scrollHeight; + } + + if (body) { + body.scrollBy(0, elementTop - offset); + } + + if (updateHistory) { + // eslint-disable-next-line no-undef + history.pushState(null, null, '#' + element.id); + } +} + +// eslint-disable-next-line no-unused-vars +function bringLinkToView(event) { + event.preventDefault(); + event.stopPropagation(); + var id = event.currentTarget.getAttribute('href'); + + if (!id) { + return; + } + + var element = document.getElementById(id.slice(1)); + + if (element) { + bringElementIntoView(element); + } +} + +function bringIdToViewOnMount() { + if (isSourcePage()) { + return; + } + + // eslint-disable-next-line no-undef + var id = window.location.hash; + + if (id === '') { + return; + } + + var element = document.getElementById(id.slice(1)); + + if (!element) { + id = decodeURI(id); + element = document.getElementById(id.slice(1)); + } + + if (element) { + bringElementIntoView(element, false); + } +} + +function createAnchorElement(id) { + var anchor = document.createElement('a'); + + anchor.textContent = '#'; + anchor.href = '#' + id; + anchor.classList.add('link-anchor'); + anchor.onclick = bringLinkToView; + + return anchor; +} + +function addAnchor() { + var main = document.querySelector('.main-content').querySelector('section'); + + var h1 = main.querySelectorAll('h1'); + var h2 = main.querySelectorAll('h2'); + var h3 = main.querySelectorAll('h3'); + var h4 = main.querySelectorAll('h4'); + + var targets = [h1, h2, h3, h4]; + + targets.forEach(function (target) { + target.forEach(function (heading) { + var anchor = createAnchorElement(heading.id); + + heading.classList.add('has-anchor'); + heading.append(anchor); + }); + }); +} + +/** + * + * @param {string} value + */ +function copy(value) { + const el = document.createElement('textarea'); + + el.value = value; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); +} + +function showTooltip(id) { + var tooltip = document.getElementById(id); + + tooltip.classList.add('show-tooltip'); + setTimeout(function () { + tooltip.classList.remove('show-tooltip'); + }, 3000); +} + +/* eslint-disable-next-line */ +function copyFunction(id) { + // selecting the pre element + var code = document.getElementById(id); + + // selecting the ol.linenums + var element = code.querySelector('.linenums'); + + if (!element) { + // selecting the code block + element = code.querySelector('code'); + } + + // copy + copy(element.innerText.trim().replace(/(^\t)/gm, '')); + + // show tooltip + showTooltip('tooltip-' + id); +} + +function hideTocOnSourcePage() { + if (isSourcePage()) { + document.querySelector('.toc-container').style.display = 'none'; + } +} + +function getPreTopBar(id, lang = '') { + // tooltip + var tooltip = '
Copied!
'; + + // template of copy to clipboard icon container + var copyToClipboard = + ''; + + var langNameDiv = + '
' + + lang.toLocaleUpperCase() + + '
'; + + var topBar = + '
' + + langNameDiv + + copyToClipboard + + '
'; + + return topBar; +} + +function getPreDiv() { + var divElement = document.createElement('div'); + + divElement.classList.add('pre-div'); + + return divElement; +} + +function processAllPre() { + var targets = document.querySelectorAll('pre'); + var footer = document.querySelector('#PeOAagUepe'); + var navbar = document.querySelector('#VuAckcnZhf'); + + var navbarHeight = 0; + var footerHeight = 0; + + if (footer) { + footerHeight = footer.getBoundingClientRect().height; + } + + if (navbar) { + navbarHeight = navbar.getBoundingClientRect().height; + } + + // eslint-disable-next-line no-undef + var preMaxHeight = window.innerHeight - navbarHeight - footerHeight - 250; + + targets.forEach(function (pre, idx) { + var parent = pre.parentNode; + + if (parent && parent.getAttribute('data-skip-pre-process') === 'true') { + return; + } + + var div = getPreDiv(); + var id = 'ScDloZOMdL' + idx; + + var lang = pre.getAttribute('data-lang') || 'code'; + var topBar = getPreTopBar(id, lang); + + div.innerHTML = topBar; + + pre.style.maxHeight = preMaxHeight + 'px'; + pre.id = id; + pre.classList.add('prettyprint'); + pre.parentNode.insertBefore(div, pre); + div.appendChild(pre); + }); +} + +function highlightAndBringLineIntoView() { + // eslint-disable-next-line no-undef + var lineNumber = window.location.hash.replace('#line', ''); + + try { + var selector = '[data-line-number="' + lineNumber + '"'; + + var element = document.querySelector(selector); + + element.scrollIntoView(); + element.parentNode.classList.add('selected'); + } catch (error) { + console.error(error); + } +} + +function getFontSize() { + var currentFontSize = 16; + + try { + currentFontSize = Number.parseInt( + html.style.fontSize.split('px')[0], + 10 + ); + } catch (error) { + console.log(error); + } + + return currentFontSize; +} + +function localUpdateFontSize(fontSize) { + html.style.fontSize = fontSize + 'px'; + + var fontSizeText = document.querySelector( + '#b77a68a492f343baabea06fad81f651e' + ); + + if (fontSizeText) { + fontSizeText.innerHTML = fontSize; + } +} + +function updateFontSize(fontSize) { + localUpdateFontSize(fontSize); + localStorage.setItem(fontSizeLocalStorageKey, fontSize); +} + +(function () { + var fontSize = getFontSize(); + var fontSizeInLocalStorage = localStorage.getItem(fontSizeLocalStorageKey); + + if (fontSizeInLocalStorage) { + var n = Number.parseInt(fontSizeInLocalStorage, 10); + + if (n === fontSize) { + return; + } + updateFontSize(n); + } else { + updateFontSize(fontSize); + } +})(); + +// eslint-disable-next-line no-unused-vars +function incrementFont(event) { + var n = getFontSize(); + + if (n < MAX_FONT_SIZE) { + updateFontSize(n + 1); + } +} + +// eslint-disable-next-line no-unused-vars +function decrementFont(event) { + var n = getFontSize(); + + if (n > MIN_FONT_SIZE) { + updateFontSize(n - 1); + } +} + +function fontSizeTooltip() { + var fontSize = getFontSize(); + + return ` +
+ +
+ ${fontSize} +
+ + +
+ + `; +} + +function initTooltip() { + // add tooltip to navbar item + // eslint-disable-next-line no-undef + tippy('.theme-toggle', { + content: 'Toggle Theme', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.search-button', { + content: 'Search', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.font-size', { + content: 'Change font size', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.codepen-button', { + content: 'Open code in CodePen', + placement: 'left', + }); + + // eslint-disable-next-line no-undef + tippy('.copy-code', { + content: 'Copy this code', + placement: 'left', + }); + + // eslint-disable-next-line no-undef + tippy('.font-size', { + content: fontSizeTooltip(), + trigger: 'click', + interactive: true, + allowHTML: true, + placement: 'left', + }); +} + +function fixTable() { + const tables = document.querySelectorAll('table'); + + for (const table of tables) { + if (table.classList.contains('hljs-ln')) { + // don't want to wrap code blocks. + return; + } + + var div = document.createElement('div'); + + div.classList.add('table-div'); + table.parentNode.insertBefore(div, table); + div.appendChild(table); + } +} + +function hideMobileMenu() { + var mobileMenuContainer = document.querySelector('#mobile-sidebar'); + var target = document.querySelector('#mobile-menu'); + var svgUse = target.querySelector('use'); + + if (mobileMenuContainer) { + mobileMenuContainer.classList.remove('show'); + } + if (target) { + target.setAttribute('data-isopen', 'false'); + } + if (svgUse) { + svgUse.setAttribute('xlink:href', '#menu-icon'); + } +} + +function showMobileMenu() { + var mobileMenuContainer = document.querySelector('#mobile-sidebar'); + var target = document.querySelector('#mobile-menu'); + var svgUse = target.querySelector('use'); + + if (mobileMenuContainer) { + mobileMenuContainer.classList.add('show'); + } + if (target) { + target.setAttribute('data-isopen', 'true'); + } + if (svgUse) { + svgUse.setAttribute('xlink:href', '#close-icon'); + } +} + +function onMobileMenuClick() { + var target = document.querySelector('#mobile-menu'); + var isOpen = target.getAttribute('data-isopen') === 'true'; + + if (isOpen) { + hideMobileMenu(); + } else { + showMobileMenu(); + } +} + +function initMobileMenu() { + var menu = document.querySelector('#mobile-menu'); + + if (menu) { + menu.addEventListener('click', onMobileMenuClick); + } +} + +function addHrefToSidebarTitle() { + var titles = document.querySelectorAll('.sidebar-title-anchor'); + + titles.forEach(function (title) { + // eslint-disable-next-line no-undef + title.setAttribute('href', baseURL); + }); +} + +function onDomContentLoaded() { + var themeButton = document.querySelectorAll('.theme-toggle'); + + initMobileMenu(); + + if (themeButton) { + themeButton.forEach(function (button) { + button.addEventListener('click', toggleTheme); + }); + } + + // Highlighting code + + // eslint-disable-next-line no-undef + hljs.addPlugin({ + 'after:highlightElement': function (obj) { + // Replace 'code' with result.language when + // we are able to cross-check the correctness of + // result. + obj.el.parentNode.setAttribute('data-lang', 'code'); + }, + }); + // eslint-disable-next-line no-undef + hljs.highlightAll(); + // eslint-disable-next-line no-undef + hljs.initLineNumbersOnLoad({ + singleLine: true, + }); + + // Highlight complete + + initAccordion(); + addAnchor(); + processAllPre(); + hideTocOnSourcePage(); + setTimeout(function () { + bringIdToViewOnMount(); + if (isSourcePage()) { + highlightAndBringLineIntoView(); + } + }, 1000); + initTooltip(); + fixTable(); + addHrefToSidebarTitle(); +} + +// eslint-disable-next-line no-undef +window.addEventListener('DOMContentLoaded', onDomContentLoaded); + +// eslint-disable-next-line no-undef +window.addEventListener('hashchange', (event) => { + const url = new URL(event.newURL); + + if (url.hash !== '') { + bringIdToViewOnMount(url.hash); + } +}); + +// eslint-disable-next-line no-undef +window.addEventListener('storage', (event) => { + if (event.newValue === 'undefined') return; + + initTooltip(); + + if (event.key === themeLocalStorageKey) localUpdateTheme(event.newValue); + if (event.key === fontSizeLocalStorageKey) + localUpdateFontSize(event.newValue); +}); diff --git a/docs/api/scripts/core.min.js b/docs/api/scripts/core.min.js new file mode 100644 index 000000000..6165f9ffa --- /dev/null +++ b/docs/api/scripts/core.min.js @@ -0,0 +1,23 @@ +var accordionLocalStorageKey="accordion-id",themeLocalStorageKey="theme",fontSizeLocalStorageKey="font-size",html=document.querySelector("html"),MAX_FONT_SIZE=30,MIN_FONT_SIZE=10,localStorage=window.localStorage;function getTheme(){var e=localStorage.getItem(themeLocalStorageKey);if(e)return e;switch(e=document.body.getAttribute("data-theme")){case"dark":case"light":return e;case"fallback-dark":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";case"fallback-light":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";default:return"dark"}}function localUpdateTheme(e){var t=document.body,o=document.querySelectorAll(".theme-svg-use"),n="dark"===e?"#light-theme-icon":"#dark-theme-icon";t.setAttribute("data-theme",e),t.classList.remove("dark","light"),t.classList.add(e),o.forEach(function(e){e.setAttribute("xlink:href",n)})}function updateTheme(e){localUpdateTheme(e),localStorage.setItem(themeLocalStorageKey,e)}function toggleTheme(){updateTheme("dark"===document.body.getAttribute("data-theme")?"light":"dark")}function setAccordionIdToLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));t[e]=e,localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function removeAccordionIdFromLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));delete t[e],localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function getAccordionIdsFromLocalStorage(){return JSON.parse(localStorage.getItem(accordionLocalStorageKey))||{}}function toggleAccordion(e){"false"===e.getAttribute("data-isopen")?(e.setAttribute("data-isopen","true"),setAccordionIdToLocalStorage(e.id)):(e.setAttribute("data-isopen","false"),removeAccordionIdFromLocalStorage(e.id))}function initAccordion(){void 0!==localStorage.getItem(accordionLocalStorageKey)&&null!==localStorage.getItem(accordionLocalStorageKey)||localStorage.setItem(accordionLocalStorageKey,"{}");var e=document.querySelectorAll(".sidebar-section-title"),t=getAccordionIdsFromLocalStorage();e.forEach(function(e){e.addEventListener("click",function(){toggleAccordion(e)}),e.id in t&&toggleAccordion(e)})}function isSourcePage(){return Boolean(document.querySelector("#source-page"))}function bringElementIntoView(e,t=!0){var o,n,i,c;e&&(tocbotInstance&&setTimeout(()=>tocbotInstance.updateTocListActiveElement(e),60),o=document.querySelector(".navbar-container"),n=document.querySelector(".main-content"),i=e.getBoundingClientRect().top,c=16,o&&(c+=o.scrollHeight),n&&n.scrollBy(0,i-c),t&&history.pushState(null,null,"#"+e.id))}function bringLinkToView(e){e.preventDefault(),e.stopPropagation();var e=e.currentTarget.getAttribute("href");!e||(e=document.getElementById(e.slice(1)))&&bringElementIntoView(e)}function bringIdToViewOnMount(){var e,t;isSourcePage()||""!==(e=window.location.hash)&&((t=document.getElementById(e.slice(1)))||(e=decodeURI(e),t=document.getElementById(e.slice(1))),t&&bringElementIntoView(t,!1))}function createAnchorElement(e){var t=document.createElement("a");return t.textContent="#",t.href="#"+e,t.classList.add("link-anchor"),t.onclick=bringLinkToView,t}function addAnchor(){var e=document.querySelector(".main-content").querySelector("section");[e.querySelectorAll("h1"),e.querySelectorAll("h2"),e.querySelectorAll("h3"),e.querySelectorAll("h4")].forEach(function(e){e.forEach(function(e){var t=createAnchorElement(e.id);e.classList.add("has-anchor"),e.append(t)})})}function copy(e){const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}function showTooltip(e){var t=document.getElementById(e);t.classList.add("show-tooltip"),setTimeout(function(){t.classList.remove("show-tooltip")},3e3)}function copyFunction(e){var t=document.getElementById(e);copy((t.querySelector(".linenums")||t.querySelector("code")).innerText.trim().replace(/(^\t)/gm,"")),showTooltip("tooltip-"+e)}function hideTocOnSourcePage(){isSourcePage()&&(document.querySelector(".toc-container").style.display="none")}function getPreTopBar(e,t=""){e='";return'
'+('
'+t.toLocaleUpperCase()+"
")+e+"
"}function getPreDiv(){var e=document.createElement("div");return e.classList.add("pre-div"),e}function processAllPre(){var e=document.querySelectorAll("pre"),t=document.querySelector("#PeOAagUepe"),o=document.querySelector("#VuAckcnZhf"),n=0,i=0,c=(t&&(i=t.getBoundingClientRect().height),o&&(n=o.getBoundingClientRect().height),window.innerHeight-n-i-250);e.forEach(function(e,t){var o,n=e.parentNode;n&&"true"===n.getAttribute("data-skip-pre-process")||(n=getPreDiv(),o=getPreTopBar(t="ScDloZOMdL"+t,e.getAttribute("data-lang")||"code"),n.innerHTML=o,e.style.maxHeight=c+"px",e.id=t,e.classList.add("prettyprint"),e.parentNode.insertBefore(n,e),n.appendChild(e))})}function highlightAndBringLineIntoView(){var e=window.location.hash.replace("#line","");try{var t='[data-line-number="'+e+'"',o=document.querySelector(t);o.scrollIntoView(),o.parentNode.classList.add("selected")}catch(e){console.error(e)}}function getFontSize(){var e=16;try{e=Number.parseInt(html.style.fontSize.split("px")[0],10)}catch(e){console.log(e)}return e}function localUpdateFontSize(e){html.style.fontSize=e+"px";var t=document.querySelector("#b77a68a492f343baabea06fad81f651e");t&&(t.innerHTML=e)}function updateFontSize(e){localUpdateFontSize(e),localStorage.setItem(fontSizeLocalStorageKey,e)}function incrementFont(e){var t=getFontSize();t + +
+ ${e} +
+ + + + + `}function initTooltip(){tippy(".theme-toggle",{content:"Toggle Theme",delay:500}),tippy(".search-button",{content:"Search",delay:500}),tippy(".font-size",{content:"Change font size",delay:500}),tippy(".codepen-button",{content:"Open code in CodePen",placement:"left"}),tippy(".copy-code",{content:"Copy this code",placement:"left"}),tippy(".font-size",{content:fontSizeTooltip(),trigger:"click",interactive:!0,allowHTML:!0,placement:"left"})}function fixTable(){for(const t of document.querySelectorAll("table")){if(t.classList.contains("hljs-ln"))return;var e=document.createElement("div");e.classList.add("table-div"),t.parentNode.insertBefore(e,t),e.appendChild(t)}}function hideMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.remove("show"),t&&t.setAttribute("data-isopen","false"),o&&o.setAttribute("xlink:href","#menu-icon")}function showMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.add("show"),t&&t.setAttribute("data-isopen","true"),o&&o.setAttribute("xlink:href","#close-icon")}function onMobileMenuClick(){("true"===document.querySelector("#mobile-menu").getAttribute("data-isopen")?hideMobileMenu:showMobileMenu)()}function initMobileMenu(){var e=document.querySelector("#mobile-menu");e&&e.addEventListener("click",onMobileMenuClick)}function addHrefToSidebarTitle(){document.querySelectorAll(".sidebar-title-anchor").forEach(function(e){e.setAttribute("href",baseURL)})}function onDomContentLoaded(){var e=document.querySelectorAll(".theme-toggle");initMobileMenu(),e&&e.forEach(function(e){e.addEventListener("click",toggleTheme)}),hljs.addPlugin({"after:highlightElement":function(e){e.el.parentNode.setAttribute("data-lang","code")}}),hljs.highlightAll(),hljs.initLineNumbersOnLoad({singleLine:!0}),initAccordion(),addAnchor(),processAllPre(),hideTocOnSourcePage(),setTimeout(function(){bringIdToViewOnMount(),isSourcePage()&&highlightAndBringLineIntoView()},1e3),initTooltip(),fixTable(),addHrefToSidebarTitle()}updateTheme(getTheme()),function(){var e=getFontSize(),t=localStorage.getItem(fontSizeLocalStorageKey);t?(t=Number.parseInt(t,10))!==e&&updateFontSize(t):updateFontSize(e)}(),window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",e=>{e=new URL(e.newURL);""!==e.hash&&bringIdToViewOnMount(e.hash)}),window.addEventListener("storage",e=>{"undefined"!==e.newValue&&(initTooltip(),e.key===themeLocalStorageKey&&localUpdateTheme(e.newValue),e.key===fontSizeLocalStorageKey&&localUpdateFontSize(e.newValue))}); \ No newline at end of file diff --git a/docs/api/scripts/resize.js b/docs/api/scripts/resize.js new file mode 100644 index 000000000..d8e3a9cf7 --- /dev/null +++ b/docs/api/scripts/resize.js @@ -0,0 +1,90 @@ +/* global document */ +// This file is @deprecated + +var NAVBAR_OPTIONS = {}; + +(function() { + var NAVBAR_RESIZE_LOCAL_STORAGE_KEY = 'NAVBAR_RESIZE_LOCAL_STORAGE_KEY'; + + var navbar = document.querySelector('#navbar'); + var footer = document.querySelector('#footer'); + var mainSection = document.querySelector('#main'); + var localStorageResizeObject = JSON.parse( + // eslint-disable-next-line no-undef + localStorage.getItem(NAVBAR_RESIZE_LOCAL_STORAGE_KEY) + ); + + /** + * Check whether we have any resize value in local storage or not. + * If we have resize value then resize the navbar. + **/ + if (localStorageResizeObject) { + navbar.style.width = localStorageResizeObject.width; + mainSection.style.marginLeft = localStorageResizeObject.width; + footer.style.marginLeft = localStorageResizeObject.width; + } + + var navbarSlider = document.querySelector('#navbar-resize'); + + function resizeNavbar(event) { + var pageX = event.pageX, + pageXPlusPx = event.pageX + 'px', + min = Number.parseInt(NAVBAR_OPTIONS.min, 10) || 300, + max = Number.parseInt(NAVBAR_OPTIONS.max, 10) || 600; + + /** + * Just to add some checks. If min is smaller than 10 then + * user may accidentally end up reducing the size of navbar + * less than 10. In that case user will not able to resize navbar + * because navbar slider will be hidden. + */ + if (min < 10) { + min = 10; + } + + /** + * Only resize if pageX in range between min and max + * allowed value. + */ + if (min < pageX && pageX < max) { + navbar.style.width = pageXPlusPx; + mainSection.style.marginLeft = pageXPlusPx; + footer.style.marginLeft = pageXPlusPx; + } + } + + function setupEventListeners() { + // eslint-disable-next-line no-undef + window.addEventListener('mousemove', resizeNavbar); + // eslint-disable-next-line no-undef + window.addEventListener('touchmove', resizeNavbar); + } + + function afterRemovingEventListeners() { + // eslint-disable-next-line no-undef + localStorage.setItem( + NAVBAR_RESIZE_LOCAL_STORAGE_KEY, + JSON.stringify({ + width: navbar.style.width + }) + ); + } + + function removeEventListeners() { + // eslint-disable-next-line no-undef + window.removeEventListener('mousemove', resizeNavbar); + // eslint-disable-next-line no-undef + window.removeEventListener('touchend', resizeNavbar); + afterRemovingEventListeners(); + } + + navbarSlider.addEventListener('mousedown', setupEventListeners); + navbarSlider.addEventListener('touchstart', setupEventListeners); + // eslint-disable-next-line no-undef + window.addEventListener('mouseup', removeEventListeners); +})(); + +// eslint-disable-next-line no-unused-vars +function setupResizeOptions(options) { + NAVBAR_OPTIONS = options; +} diff --git a/docs/api/scripts/search.js b/docs/api/scripts/search.js new file mode 100644 index 000000000..415e1cf8d --- /dev/null +++ b/docs/api/scripts/search.js @@ -0,0 +1,265 @@ +/* global document */ + +const searchId = 'LiBfqbJVcV'; +const searchHash = '#' + searchId; +const searchContainer = document.querySelector('#PkfLWpAbet'); +const searchWrapper = document.querySelector('#iCxFxjkHbP'); +const searchCloseButton = document.querySelector('#VjLlGakifb'); +const searchInput = document.querySelector('#vpcKVYIppa'); +const resultBox = document.querySelector('#fWwVHRuDuN'); + +function showResultText(text) { + resultBox.innerHTML = `${text}`; +} + +function hideSearch() { + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + // eslint-disable-next-line no-undef + history.go(-1); + } + + // eslint-disable-next-line no-undef + window.onhashchange = null; + + if (searchContainer) { + searchContainer.style.display = 'none'; + } +} + +function listenCloseKey(event) { + if (event.key === 'Escape') { + hideSearch(); + // eslint-disable-next-line no-undef + window.removeEventListener('keyup', listenCloseKey); + } +} + +function showSearch() { + try { + // Closing mobile menu before opening + // search box. + // It is defined in core.js + // eslint-disable-next-line no-undef + hideMobileMenu(); + } catch (error) { + console.error(error); + } + + // eslint-disable-next-line no-undef + window.onhashchange = hideSearch; + + // eslint-disable-next-line no-undef + if (window.location.hash !== searchHash) { + // eslint-disable-next-line no-undef + history.pushState(null, null, searchHash); + } + + if (searchContainer) { + searchContainer.style.display = 'flex'; + // eslint-disable-next-line no-undef + window.addEventListener('keyup', listenCloseKey); + } + + if (searchInput) { + searchInput.focus(); + } +} + +async function fetchAllData() { + // eslint-disable-next-line no-undef + const { hostname, protocol, port } = location; + + // eslint-disable-next-line no-undef + const base = protocol + '//' + hostname + (port !== '' ? ':' + port : '') + baseURL; + // eslint-disable-next-line no-undef + const url = new URL('data/search.json', base); + const result = await fetch(url); + const { list } = await result.json(); + + return list; +} + +// eslint-disable-next-line no-unused-vars +function onClickSearchItem(event) { + const target = event.currentTarget; + + if (target) { + const href = target.getAttribute('href') || ''; + let elementId = href.split('#')[1] || ''; + let element = document.getElementById(elementId); + + if (!element) { + elementId = decodeURI(elementId); + element = document.getElementById(elementId); + } + + if (element) { + setTimeout(function() { + // eslint-disable-next-line no-undef + bringElementIntoView(element); // defined in core.js + }, 100); + } + } +} + +function buildSearchResult(result) { + let output = ''; + const removeHTMLTagsRegExp = /(<([^>]+)>)/ig; + + for (const res of result) { + const { title = '', description = '' } = res.item; + + const _link = res.item.link.replace('.*/, ''); + const _title = title.replace(removeHTMLTagsRegExp, ""); + const _description = description.replace(removeHTMLTagsRegExp, ""); + + output += ` + +
${_title}
+
${_description || 'No description available.'}
+
+ `; + } + + return output; +} + +function getSearchResult(list, keys, searchKey) { + const defaultOptions = { + shouldSort: true, + threshold: 0.4, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: keys + }; + + const options = { ...defaultOptions }; + + // eslint-disable-next-line no-undef + const searchIndex = Fuse.createIndex(options.keys, list); + + // eslint-disable-next-line no-undef + const fuse = new Fuse(list, options, searchIndex); + + const result = fuse.search(searchKey); + + if (result.length > 20) { + return result.slice(0, 20); + } + + return result; +} + +function debounce(func, wait, immediate) { + let timeout; + + return function() { + const args = arguments; + + clearTimeout(timeout); + timeout = setTimeout(() => { + timeout = null; + if (!immediate) { + // eslint-disable-next-line consistent-this, no-invalid-this + func.apply(this, args); + } + }, wait); + + if (immediate && !timeout) { + // eslint-disable-next-line consistent-this, no-invalid-this + func.apply(this, args); + } + }; +} + +let searchData; + +async function search(event) { + const value = event.target.value; + const keys = ['title', 'description']; + + if (!resultBox) { + console.error('Search result container not found'); + + return; + } + + if (!value) { + showResultText('Type anything to view search result'); + + return; + } + + if (!searchData) { + showResultText('Loading...'); + + try { + // eslint-disable-next-line require-atomic-updates + searchData = await fetchAllData(); + } catch (e) { + console.log(e); + showResultText('Failed to load result.'); + + return; + } + } + + const result = getSearchResult(searchData, keys, value); + + if (!result.length) { + showResultText('No result found! Try some different combination.'); + + return; + } + + // eslint-disable-next-line require-atomic-updates + resultBox.innerHTML = buildSearchResult(result); +} + +function onDomContentLoaded() { + const searchButton = document.querySelectorAll('.search-button'); + const debouncedSearch = debounce(search, 300); + + if (searchCloseButton) { + searchCloseButton.addEventListener('click', hideSearch); + } + + if (searchButton) { + searchButton.forEach(function(item) { + item.addEventListener('click', showSearch); + }); + } + + if (searchContainer) { + searchContainer.addEventListener('click', hideSearch); + } + + if (searchWrapper) { + searchWrapper.addEventListener('click', function(event) { + event.stopPropagation(); + }); + } + + if (searchInput) { + searchInput.addEventListener('keyup', debouncedSearch); + } + + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + showSearch(); + } +} + +// eslint-disable-next-line no-undef +window.addEventListener('DOMContentLoaded', onDomContentLoaded); + +// eslint-disable-next-line no-undef +window.addEventListener('hashchange', function() { + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + showSearch(); + } +}); diff --git a/docs/api/scripts/search.min.js b/docs/api/scripts/search.min.js new file mode 100644 index 000000000..5358bced8 --- /dev/null +++ b/docs/api/scripts/search.min.js @@ -0,0 +1,6 @@ +const searchId="LiBfqbJVcV",searchHash="#"+searchId,searchContainer=document.querySelector("#PkfLWpAbet"),searchWrapper=document.querySelector("#iCxFxjkHbP"),searchCloseButton=document.querySelector("#VjLlGakifb"),searchInput=document.querySelector("#vpcKVYIppa"),resultBox=document.querySelector("#fWwVHRuDuN");function showResultText(e){resultBox.innerHTML=`${e}`}function hideSearch(){window.location.hash===searchHash&&history.go(-1),window.onhashchange=null,searchContainer&&(searchContainer.style.display="none")}function listenCloseKey(e){"Escape"===e.key&&(hideSearch(),window.removeEventListener("keyup",listenCloseKey))}function showSearch(){try{hideMobileMenu()}catch(e){console.error(e)}window.onhashchange=hideSearch,window.location.hash!==searchHash&&history.pushState(null,null,searchHash),searchContainer&&(searchContainer.style.display="flex",window.addEventListener("keyup",listenCloseKey)),searchInput&&searchInput.focus()}async function fetchAllData(){var{hostname:e,protocol:t,port:n}=location,t=t+"//"+e+(""!==n?":"+n:"")+baseURL,e=new URL("data/search.json",t);const a=await fetch(e);n=(await a.json()).list;return n}function onClickSearchItem(t){const n=t.currentTarget;if(n){const a=n.getAttribute("href")||"";t=a.split("#")[1]||"";let e=document.getElementById(t);e||(t=decodeURI(t),e=document.getElementById(t)),e&&setTimeout(function(){bringElementIntoView(e)},100)}}function buildSearchResult(e){let t="";var n=/(<([^>]+)>)/gi;for(const s of e){const{title:c="",description:i=""}=s.item;var a=s.item.link.replace('.*/,""),o=c.replace(n,""),r=i.replace(n,"");t+=` + +
${o}
+
${r||"No description available."}
+
+ `}return t}function getSearchResult(e,t,n){var t={...{shouldSort:!0,threshold:.4,location:0,distance:100,maxPatternLength:32,minMatchCharLength:1,keys:t}},a=Fuse.createIndex(t.keys,e);const o=new Fuse(e,t,a),r=o.search(n);return 20{o=null,a||t.apply(this,e)},n),a&&!o&&t.apply(this,e)}}let searchData;async function search(e){e=e.target.value;if(resultBox)if(e){if(!searchData){showResultText("Loading...");try{searchData=await fetchAllData()}catch(e){return console.log(e),void showResultText("Failed to load result.")}}e=getSearchResult(searchData,["title","description"],e);e.length?resultBox.innerHTML=buildSearchResult(e):showResultText("No result found! Try some different combination.")}else showResultText("Type anything to view search result");else console.error("Search result container not found")}function onDomContentLoaded(){const e=document.querySelectorAll(".search-button");var t=debounce(search,300);searchCloseButton&&searchCloseButton.addEventListener("click",hideSearch),e&&e.forEach(function(e){e.addEventListener("click",showSearch)}),searchContainer&&searchContainer.addEventListener("click",hideSearch),searchWrapper&&searchWrapper.addEventListener("click",function(e){e.stopPropagation()}),searchInput&&searchInput.addEventListener("keyup",t),window.location.hash===searchHash&&showSearch()}window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",function(){window.location.hash===searchHash&&showSearch()}); \ No newline at end of file diff --git a/docs/api/scripts/third-party/Apache-License-2.0.txt b/docs/api/scripts/third-party/Apache-License-2.0.txt new file mode 100644 index 000000000..75b52484e --- /dev/null +++ b/docs/api/scripts/third-party/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/api/scripts/third-party/fuse.js b/docs/api/scripts/third-party/fuse.js new file mode 100644 index 000000000..a55c5daa0 --- /dev/null +++ b/docs/api/scripts/third-party/fuse.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.4.6 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2021 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:3,t=new Map,n=Math.pow(10,e);return{get:function(e){var r=e.match(I).length;if(t.has(r))return t.get(r);var i=1/Math.sqrt(r),o=parseFloat(Math.round(i*n)/n);return t.set(r,o),o},clear:function(){t.clear()}}}var E=function(){function e(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.getFn,i=void 0===r?A.getFn:r;t(this,e),this.norm=C(3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return r(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?A.getFn:r,o=new E({getFn:i});return o.setKeys(e.map(_)),o.setSources(t),o.create(),o}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?A.distance:s,h=t.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r/e.length;if(f)return l;var d=Math.abs(a-o);return u?l+d/u:d?1:l}function F(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:A.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}function P(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?A.location:o,a=i.threshold,s=void 0===a?A.threshold:a,u=i.distance,h=void 0===u?A.distance:u,f=i.includeMatches,l=void 0===f?A.includeMatches:f,d=i.findAllMatches,v=void 0===d?A.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?A.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?A.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?A.ignoreLocation:k;if(t(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:l,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?n:n.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){r.chunks.push({pattern:e,alphabet:P(e),startIndex:t})},x=this.pattern.length;if(x>32){for(var L=0,S=x%32,w=x-S;L3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?A.location:i,c=r.distance,a=void 0===c?A.distance:c,s=r.threshold,u=void 0===s?A.threshold:s,h=r.findAllMatches,f=void 0===h?A.findAllMatches:h,l=r.minMatchCharLength,d=void 0===l?A.minMatchCharLength:l,v=r.includeMatches,g=void 0===v?A.includeMatches:v,y=r.ignoreLocation,p=void 0===y?A.ignoreLocation:y;if(t.length>32)throw new Error(L(32));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,S=b,w=d>1||g,_=w?Array(M):[];(m=e.indexOf(t,S))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),S=m+k,w)for(var j=0;j=K;J-=1){var T=J-1,U=n[e.charAt(T)];if(w&&(_[T]=+!!U),W[J]=(W[J+1]<<1|1)&U,P&&(W[J]|=(I[J+1]|I[J])<<1|1|I[J+1]),W[J]&$&&(C=R(t,{errors:P,currentLocation:T,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(S=T)<=b)break;K=Math.max(1,2*b-S)}}var V=R(t,{errors:P+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p});if(V>x)break;I=W}var B={isMatch:S>=0,score:Math.max(.001,C)};if(w){var G=F(_,d);G.length?g&&(B.indices=G):B.isMatch=!1}return B}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:f}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(l(d),l(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),D=function(){function e(n){t(this,e),this.pattern=n}return r(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return z(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return z(e,this.singleRegex)}}]),e}();function z(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),i}(D),q=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),i}(D),W=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),i}(D),J=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),i}(D),T=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),i}(D),U=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),i}(D),V=function(e){a(i,e);var n=f(i);function i(e){var r,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?A.location:c,s=o.threshold,u=void 0===s?A.threshold:s,h=o.distance,f=void 0===h?A.distance:h,l=o.includeMatches,d=void 0===l?A.includeMatches:l,v=o.findAllMatches,g=void 0===v?A.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?A.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?A.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?A.ignoreLocation:M;return t(this,i),(r=n.call(this,e))._bitapSearch=new N(e,{location:a,threshold:u,distance:f,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),r}return r(i,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),i}(D),B=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),i}(D),G=[K,B,W,J,U,T,q,V],H=G.length,Q=/ +(?=([^\"]*\"[^\"]*\")*[^\"]*$)/;function X(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Q).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=r.isCaseSensitive,o=void 0===i?A.isCaseSensitive:i,c=r.includeMatches,a=void 0===c?A.includeMatches:c,s=r.minMatchCharLength,u=void 0===s?A.minMatchCharLength:s,h=r.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r.findAllMatches,d=void 0===l?A.findAllMatches:l,v=r.location,g=void 0===v?A.location:v,y=r.threshold,p=void 0===y?A.threshold:y,m=r.distance,k=void 0===m?A.distance:m;t(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:f,location:g,threshold:p,distance:k},this.pattern=o?n:n.toLowerCase(),this.query=X(this.pattern,this.options)}return r(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function le(e,t){t.score=e.score}function de(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?A.includeMatches:r,o=n.includeScore,c=void 0===o?A.includeScore:o,a=[];return i&&a.push(fe),c&&a.push(le),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ve=function(){function e(n){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;t(this,e),this.options=c({},A,{},r),this.options.useExtendedSearch,this._keyStore=new w(this.options.keys),this.setCollection(n,i)}return r(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof E))throw new Error("Incorrect 'index' type");this._myIndex=t||$(this.options.keys,this._docs,{getFn:this.options.getFn})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return he(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),de(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=te(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.auto,i=void 0===r||r,o=function e(n){var r=Object.keys(n),o=ae(n);if(!o&&r.length>1&&!ce(n))return e(ue(n));if(se(n)){var c=o?n[ie]:r[0],a=o?n[oe]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return i&&(s.searcher=te(a,t)),s}var u={children:[],operator:r[0]};return r.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return ce(e)||(e=ue(e)),o(e)}(e,this.options),r=this._myIndex.records,i={},o=[];return r.forEach((function(e){var r=e.$,c=e.i;if(k(r)){var a=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}switch(n.operator){case ne:for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?A.getFn:n,i=e.keys,o=e.records,c=new E({getFn:r});return c.setKeys(i),c.setIndexRecords(o),c},ve.config=A,function(){ee.push.apply(ee,arguments)}(Z),ve},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Fuse=t(); \ No newline at end of file diff --git a/docs/api/scripts/third-party/hljs-line-num-original.js b/docs/api/scripts/third-party/hljs-line-num-original.js new file mode 100644 index 000000000..9b8e18f77 --- /dev/null +++ b/docs/api/scripts/third-party/hljs-line-num-original.js @@ -0,0 +1,369 @@ +// jshint multistr:true + +(function (w, d) { + 'use strict'; + + var TABLE_NAME = 'hljs-ln', + LINE_NAME = 'hljs-ln-line', + CODE_BLOCK_NAME = 'hljs-ln-code', + NUMBERS_BLOCK_NAME = 'hljs-ln-numbers', + NUMBER_LINE_NAME = 'hljs-ln-n', + DATA_ATTR_NAME = 'data-line-number', + BREAK_LINE_REGEXP = /\r\n|\r|\n/g; + + if (w.hljs) { + w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad; + w.hljs.lineNumbersBlock = lineNumbersBlock; + w.hljs.lineNumbersValue = lineNumbersValue; + + addStyles(); + } else { + w.console.error('highlight.js not detected!'); + } + + function isHljsLnCodeDescendant(domElt) { + var curElt = domElt; + while (curElt) { + if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) { + return true; + } + curElt = curElt.parentNode; + } + return false; + } + + function getHljsLnTable(hljsLnDomElt) { + var curElt = hljsLnDomElt; + while (curElt.nodeName !== 'TABLE') { + curElt = curElt.parentNode; + } + return curElt; + } + + // Function to workaround a copy issue with Microsoft Edge. + // Due to hljs-ln wrapping the lines of code inside a element, + // itself wrapped inside a
 element, window.getSelection().toString()
+    // does not contain any line breaks. So we need to get them back using the
+    // rendered code in the DOM as reference.
+    function edgeGetSelectedCodeLines(selection) {
+        // current selected text without line breaks
+        var selectionText = selection.toString();
+
+        // get the 
' + + '' + + '' + + '', + [ + LINE_NAME, + NUMBERS_BLOCK_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME, + CODE_BLOCK_NAME, + i + options.startFrom, + lines[i].length > 0 ? lines[i] : ' ' + ]); + } + + return format('
element wrapping the first line of selected code + var tdAnchor = selection.anchorNode; + while (tdAnchor.nodeName !== 'TD') { + tdAnchor = tdAnchor.parentNode; + } + + // get the element wrapping the last line of selected code + var tdFocus = selection.focusNode; + while (tdFocus.nodeName !== 'TD') { + tdFocus = tdFocus.parentNode; + } + + // extract line numbers + var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber); + var lastLineNumber = parseInt(tdFocus.dataset.lineNumber); + + // multi-lines copied case + if (firstLineNumber != lastLineNumber) { + + var firstLineText = tdAnchor.textContent; + var lastLineText = tdFocus.textContent; + + // if the selection was made backward, swap values + if (firstLineNumber > lastLineNumber) { + var tmp = firstLineNumber; + firstLineNumber = lastLineNumber; + lastLineNumber = tmp; + tmp = firstLineText; + firstLineText = lastLineText; + lastLineText = tmp; + } + + // discard not copied characters in first line + while (selectionText.indexOf(firstLineText) !== 0) { + firstLineText = firstLineText.slice(1); + } + + // discard not copied characters in last line + while (selectionText.lastIndexOf(lastLineText) === -1) { + lastLineText = lastLineText.slice(0, -1); + } + + // reconstruct and return the real copied text + var selectedText = firstLineText; + var hljsLnTable = getHljsLnTable(tdAnchor); + for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) { + var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]); + var codeLineElt = hljsLnTable.querySelector(codeLineSel); + selectedText += '\n' + codeLineElt.textContent; + } + selectedText += '\n' + lastLineText; + return selectedText; + // single copied line case + } else { + return selectionText; + } + } + + // ensure consistent code copy/paste behavior across all browsers + // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51) + document.addEventListener('copy', function(e) { + // get current selection + var selection = window.getSelection(); + // override behavior when one wants to copy line of codes + if (isHljsLnCodeDescendant(selection.anchorNode)) { + var selectionText; + // workaround an issue with Microsoft Edge as copied line breaks + // are removed otherwise from the selection string + if (window.navigator.userAgent.indexOf('Edge') !== -1) { + selectionText = edgeGetSelectedCodeLines(selection); + } else { + // other browsers can directly use the selection string + selectionText = selection.toString(); + } + e.clipboardData.setData( + 'text/plain', + selectionText + .replace(/(^\t)/gm, '') + ); + e.preventDefault(); + } + }); + + function addStyles () { + var css = d.createElement('style'); + css.type = 'text/css'; + css.innerHTML = format( + '.{0}{border-collapse:collapse}' + + '.{0} td{padding:0}' + + '.{1}:before{content:attr({2})}', + [ + TABLE_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME + ]); + d.getElementsByTagName('head')[0].appendChild(css); + } + + function initLineNumbersOnLoad (options) { + if (d.readyState === 'interactive' || d.readyState === 'complete') { + documentReady(options); + } else { + w.addEventListener('DOMContentLoaded', function () { + documentReady(options); + }); + } + } + + function documentReady (options) { + try { + var blocks = d.querySelectorAll('code.hljs,code.nohighlight'); + + for (var i in blocks) { + if (blocks.hasOwnProperty(i)) { + if (!isPluginDisabledForBlock(blocks[i])) { + lineNumbersBlock(blocks[i], options); + } + } + } + } catch (e) { + w.console.error('LineNumbers error: ', e); + } + } + + function isPluginDisabledForBlock(element) { + return element.classList.contains('nohljsln'); + } + + function lineNumbersBlock (element, options) { + if (typeof element !== 'object') return; + + async(function () { + element.innerHTML = lineNumbersInternal(element, options); + }); + } + + function lineNumbersValue (value, options) { + if (typeof value !== 'string') return; + + var element = document.createElement('code') + element.innerHTML = value + + return lineNumbersInternal(element, options); + } + + function lineNumbersInternal (element, options) { + + var internalOptions = mapOptions(element, options); + + duplicateMultilineNodes(element); + + return addLineNumbersBlockFor(element.innerHTML, internalOptions); + } + + function addLineNumbersBlockFor (inputHtml, options) { + var lines = getLines(inputHtml); + + // if last line contains only carriage return remove it + if (lines[lines.length-1].trim() === '') { + lines.pop(); + } + + if (lines.length > 1 || options.singleLine) { + var html = ''; + + for (var i = 0, l = lines.length; i < l; i++) { + html += format( + '
' + + '' + + '{6}' + + '
{1}
', [ TABLE_NAME, html ]); + } + + return inputHtml; + } + + /** + * @param {HTMLElement} element Code block. + * @param {Object} options External API options. + * @returns {Object} Internal API options. + */ + function mapOptions (element, options) { + options = options || {}; + return { + singleLine: getSingleLineOption(options), + startFrom: getStartFromOption(element, options) + }; + } + + function getSingleLineOption (options) { + var defaultValue = false; + if (!!options.singleLine) { + return options.singleLine; + } + return defaultValue; + } + + function getStartFromOption (element, options) { + var defaultValue = 1; + var startFrom = defaultValue; + + if (isFinite(options.startFrom)) { + startFrom = options.startFrom; + } + + // can be overridden because local option is priority + var value = getAttribute(element, 'data-ln-start-from'); + if (value !== null) { + startFrom = toNumber(value, defaultValue); + } + + return startFrom; + } + + /** + * Recursive method for fix multi-line elements implementation in highlight.js + * Doing deep passage on child nodes. + * @param {HTMLElement} element + */ + function duplicateMultilineNodes (element) { + var nodes = element.childNodes; + for (var node in nodes) { + if (nodes.hasOwnProperty(node)) { + var child = nodes[node]; + if (getLinesCount(child.textContent) > 0) { + if (child.childNodes.length > 0) { + duplicateMultilineNodes(child); + } else { + duplicateMultilineNode(child.parentNode); + } + } + } + } + } + + /** + * Method for fix multi-line elements implementation in highlight.js + * @param {HTMLElement} element + */ + function duplicateMultilineNode (element) { + var className = element.className; + + if ( ! /hljs-/.test(className)) return; + + var lines = getLines(element.innerHTML); + + for (var i = 0, result = ''; i < lines.length; i++) { + var lineText = lines[i].length > 0 ? lines[i] : ' '; + result += format('{1}\n', [ className, lineText ]); + } + + element.innerHTML = result.trim(); + } + + function getLines (text) { + if (text.length === 0) return []; + return text.split(BREAK_LINE_REGEXP); + } + + function getLinesCount (text) { + return (text.trim().match(BREAK_LINE_REGEXP) || []).length; + } + + /// + /// HELPERS + /// + + function async (func) { + w.setTimeout(func, 0); + } + + /** + * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript} + * @param {string} format + * @param {array} args + */ + function format (format, args) { + return format.replace(/\{(\d+)\}/g, function(m, n){ + return args[n] !== undefined ? args[n] : m; + }); + } + + /** + * @param {HTMLElement} element Code block. + * @param {String} attrName Attribute name. + * @returns {String} Attribute value or empty. + */ + function getAttribute (element, attrName) { + return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null; + } + + /** + * @param {String} str Source string. + * @param {Number} fallback Fallback value. + * @returns Parsed number or fallback value. + */ + function toNumber (str, fallback) { + if (!str) return fallback; + var number = Number(str); + return isFinite(number) ? number : fallback; + } + +}(window, document)); diff --git a/docs/api/scripts/third-party/hljs-line-num.js b/docs/api/scripts/third-party/hljs-line-num.js new file mode 100644 index 000000000..facdf6bed --- /dev/null +++ b/docs/api/scripts/third-party/hljs-line-num.js @@ -0,0 +1 @@ +!function(r,o){"use strict";var e,l="hljs-ln",s="hljs-ln-line",f="hljs-ln-code",c="hljs-ln-numbers",u="hljs-ln-n",h="data-line-number",n=/\r\n|\r|\n/g;function t(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var e=parseInt(t.dataset.lineNumber),o=parseInt(r.dataset.lineNumber);if(e==o)return n;var a,i=t.textContent,l=r.textContent;for(o{6}',[s,c,u,h,f,a+t.startFrom,0{1}',[l,o])}return e}function m(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** + * performs a shallow merge of multiple objects into one + * + * @template T + * @param {T} original + * @param {Record[]} objects + * @returns {T} a single new object + */ + function inherit$1(original, ...objects) { + /** @type Record */ + const result = Object.create(null); + + for (const key in original) { + result[key] = original[key]; + } + objects.forEach(function (obj) { + for (const key in obj) { + result[key] = obj[key]; + } + }); + return /** @type {T} */ (result); + } + + /** + * @typedef {object} Renderer + * @property {(text: string) => void} addText + * @property {(node: Node) => void} openNode + * @property {(node: Node) => void} closeNode + * @property {() => string} value + */ + + /** @typedef {{kind?: string, sublanguage?: boolean}} Node */ + /** @typedef {{walk: (r: Renderer) => void}} Tree */ + /** */ + + const SPAN_CLOSE = ''; + + /** + * Determines if a node needs to be wrapped in + * + * @param {Node} node */ + const emitsWrappingTags = (node) => { + return !!node.kind; + }; + + /** + * + * @param {string} name + * @param {{prefix:string}} options + */ + const expandScopeName = (name, { prefix }) => { + if (name.includes(".")) { + const pieces = name.split("."); + return [ + `${prefix}${pieces.shift()}`, + ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)) + ].join(" "); + } + return `${prefix}${name}`; + }; + + /** @type {Renderer} */ + class HTMLRenderer { + /** + * Creates a new HTMLRenderer + * + * @param {Tree} parseTree - the parse tree (must support `walk` API) + * @param {{classPrefix: string}} options + */ + constructor(parseTree, options) { + this.buffer = ""; + this.classPrefix = options.classPrefix; + parseTree.walk(this); + } + + /** + * Adds texts to the output stream + * + * @param {string} text */ + addText(text) { + this.buffer += escapeHTML(text); + } + + /** + * Adds a node open to the output stream (if needed) + * + * @param {Node} node */ + openNode(node) { + if (!emitsWrappingTags(node)) return; + + let scope = node.kind; + if (node.sublanguage) { + scope = `language-${scope}`; + } else { + scope = expandScopeName(scope, { prefix: this.classPrefix }); + } + this.span(scope); + } + + /** + * Adds a node close to the output stream (if needed) + * + * @param {Node} node */ + closeNode(node) { + if (!emitsWrappingTags(node)) return; + + this.buffer += SPAN_CLOSE; + } + + /** + * returns the accumulated buffer + */ + value() { + return this.buffer; + } + + // helpers + + /** + * Builds a span element + * + * @param {string} className */ + span(className) { + this.buffer += ``; + } + } + + /** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} | string} Node */ + /** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} } DataNode */ + /** @typedef {import('highlight.js').Emitter} Emitter */ + /** */ + + class TokenTree { + constructor() { + /** @type DataNode */ + this.rootNode = { children: [] }; + this.stack = [this.rootNode]; + } + + get top() { + return this.stack[this.stack.length - 1]; + } + + get root() { return this.rootNode; } + + /** @param {Node} node */ + add(node) { + this.top.children.push(node); + } + + /** @param {string} kind */ + openNode(kind) { + /** @type Node */ + const node = { kind, children: [] }; + this.add(node); + this.stack.push(node); + } + + closeNode() { + if (this.stack.length > 1) { + return this.stack.pop(); + } + // eslint-disable-next-line no-undefined + return undefined; + } + + closeAllNodes() { + while (this.closeNode()); + } + + toJSON() { + return JSON.stringify(this.rootNode, null, 4); + } + + /** + * @typedef { import("./html_renderer").Renderer } Renderer + * @param {Renderer} builder + */ + walk(builder) { + // this does not + return this.constructor._walk(builder, this.rootNode); + // this works + // return TokenTree._walk(builder, this.rootNode); + } + + /** + * @param {Renderer} builder + * @param {Node} node + */ + static _walk(builder, node) { + if (typeof node === "string") { + builder.addText(node); + } else if (node.children) { + builder.openNode(node); + node.children.forEach((child) => this._walk(builder, child)); + builder.closeNode(node); + } + return builder; + } + + /** + * @param {Node} node + */ + static _collapse(node) { + if (typeof node === "string") return; + if (!node.children) return; + + if (node.children.every(el => typeof el === "string")) { + // node.text = node.children.join(""); + // delete node.children; + node.children = [node.children.join("")]; + } else { + node.children.forEach((child) => { + TokenTree._collapse(child); + }); + } + } + } + + /** + Currently this is all private API, but this is the minimal API necessary + that an Emitter must implement to fully support the parser. + + Minimal interface: + + - addKeyword(text, kind) + - addText(text) + - addSublanguage(emitter, subLanguageName) + - finalize() + - openNode(kind) + - closeNode() + - closeAllNodes() + - toHTML() + + */ + + /** + * @implements {Emitter} + */ + class TokenTreeEmitter extends TokenTree { + /** + * @param {*} options + */ + constructor(options) { + super(); + this.options = options; + } + + /** + * @param {string} text + * @param {string} kind + */ + addKeyword(text, kind) { + if (text === "") { return; } + + this.openNode(kind); + this.addText(text); + this.closeNode(); + } + + /** + * @param {string} text + */ + addText(text) { + if (text === "") { return; } + + this.add(text); + } + + /** + * @param {Emitter & {root: DataNode}} emitter + * @param {string} name + */ + addSublanguage(emitter, name) { + /** @type DataNode */ + const node = emitter.root; + node.kind = name; + node.sublanguage = true; + this.add(node); + } + + toHTML() { + const renderer = new HTMLRenderer(this, this.options); + return renderer.value(); + } + + finalize() { + return true; + } + } + + /** + * @param {string} value + * @returns {RegExp} + * */ + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function source(re) { + if (!re) return null; + if (typeof re === "string") return re; + + return re.source; + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function lookahead(re) { + return concat('(?=', re, ')'); + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function optional(re) { + return concat('(?:', re, ')?'); + } + + /** + * @param {...(RegExp | string) } args + * @returns {string} + */ + function concat(...args) { + const joined = args.map((x) => source(x)).join(""); + return joined; + } + + function stripOptionsFromArgs(args) { + const opts = args[args.length - 1]; + + if (typeof opts === 'object' && opts.constructor === Object) { + args.splice(args.length - 1, 1); + return opts; + } else { + return {}; + } + } + + /** + * Any of the passed expresssions may match + * + * Creates a huge this | this | that | that match + * @param {(RegExp | string)[] } args + * @returns {string} + */ + function either(...args) { + const opts = stripOptionsFromArgs(args); + const joined = '(' + + (opts.capture ? "" : "?:") + + args.map((x) => source(x)).join("|") + ")"; + return joined; + } + + /** + * @param {RegExp} re + * @returns {number} + */ + function countMatchGroups(re) { + return (new RegExp(re.toString() + '|')).exec('').length - 1; + } + + /** + * Does lexeme start with a regular expression match at the beginning + * @param {RegExp} re + * @param {string} lexeme + */ + function startsWith(re, lexeme) { + const match = re && re.exec(lexeme); + return match && match.index === 0; + } + + // BACKREF_RE matches an open parenthesis or backreference. To avoid + // an incorrect parse, it additionally matches the following: + // - [...] elements, where the meaning of parentheses and escapes change + // - other escape sequences, so we do not misparse escape sequences as + // interesting elements + // - non-matching or lookahead parentheses, which do not capture. These + // follow the '(' with a '?'. + const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + + // **INTERNAL** Not intended for outside usage + // join logically computes regexps.join(separator), but fixes the + // backreferences so they continue to match. + // it also places each individual regular expression into it's own + // match group, keeping track of the sequencing of those match groups + // is currently an exercise for the caller. :-) + /** + * @param {(string | RegExp)[]} regexps + * @param {{joinWith: string}} opts + * @returns {string} + */ + function _rewriteBackreferences(regexps, { joinWith }) { + let numCaptures = 0; + + return regexps.map((regex) => { + numCaptures += 1; + const offset = numCaptures; + let re = source(regex); + let out = ''; + + while (re.length > 0) { + const match = BACKREF_RE.exec(re); + if (!match) { + out += re; + break; + } + out += re.substring(0, match.index); + re = re.substring(match.index + match[0].length); + if (match[0][0] === '\\' && match[1]) { + // Adjust the backreference. + out += '\\' + String(Number(match[1]) + offset); + } else { + out += match[0]; + if (match[0] === '(') { + numCaptures++; + } + } + } + return out; + }).map(re => `(${re})`).join(joinWith); + } + + /** @typedef {import('highlight.js').Mode} Mode */ + /** @typedef {import('highlight.js').ModeCallback} ModeCallback */ + + // Common regexps + const MATCH_NOTHING_RE = /\b\B/; + const IDENT_RE$1 = '[a-zA-Z]\\w*'; + const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; + const NUMBER_RE = '\\b\\d+(\\.\\d+)?'; + const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float + const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... + const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; + + /** + * @param { Partial & {binary?: string | RegExp} } opts + */ + const SHEBANG = (opts = {}) => { + const beginShebang = /^#![ ]*\//; + if (opts.binary) { + opts.begin = concat( + beginShebang, + /.*\b/, + opts.binary, + /\b.*/); + } + return inherit$1({ + scope: 'meta', + begin: beginShebang, + end: /$/, + relevance: 0, + /** @type {ModeCallback} */ + "on:begin": (m, resp) => { + if (m.index !== 0) resp.ignoreMatch(); + } + }, opts); + }; + + // Common modes + const BACKSLASH_ESCAPE = { + begin: '\\\\[\\s\\S]', relevance: 0 + }; + const APOS_STRING_MODE = { + scope: 'string', + begin: '\'', + end: '\'', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] + }; + const QUOTE_STRING_MODE = { + scope: 'string', + begin: '"', + end: '"', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] + }; + const PHRASAL_WORDS_MODE = { + begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + }; + /** + * Creates a comment mode + * + * @param {string | RegExp} begin + * @param {string | RegExp} end + * @param {Mode | {}} [modeOptions] + * @returns {Partial} + */ + const COMMENT = function (begin, end, modeOptions = {}) { + const mode = inherit$1( + { + scope: 'comment', + begin, + end, + contains: [] + }, + modeOptions + ); + mode.contains.push({ + scope: 'doctag', + // hack to avoid the space from being included. the space is necessary to + // match here to prevent the plain text rule below from gobbling up doctags + begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)', + end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, + excludeBegin: true, + relevance: 0 + }); + const ENGLISH_WORD = either( + // list of common 1 and 2 letter words in English + "I", + "a", + "is", + "so", + "us", + "to", + "at", + "if", + "in", + "it", + "on", + // note: this is not an exhaustive list of contractions, just popular ones + /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc + /[A-Za-z]+[-][a-z]+/, // `no-way`, etc. + /[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences + ); + // looking like plain text, more likely to be a comment + mode.contains.push( + { + // TODO: how to include ", (, ) without breaking grammars that use these for + // comment delimiters? + // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/ + // --- + + // this tries to find sequences of 3 english words in a row (without any + // "programming" type syntax) this gives us a strong signal that we've + // TRULY found a comment - vs perhaps scanning with the wrong language. + // It's possible to find something that LOOKS like the start of the + // comment - but then if there is no readable text - good chance it is a + // false match and not a comment. + // + // for a visual example please see: + // https://github.com/highlightjs/highlight.js/issues/2827 + + begin: concat( + /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */ + '(', + ENGLISH_WORD, + /[.]?[:]?([.][ ]|[ ])/, + '){3}') // look for 3 words in a row + } + ); + return mode; + }; + const C_LINE_COMMENT_MODE = COMMENT('//', '$'); + const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/'); + const HASH_COMMENT_MODE = COMMENT('#', '$'); + const NUMBER_MODE = { + scope: 'number', + begin: NUMBER_RE, + relevance: 0 + }; + const C_NUMBER_MODE = { + scope: 'number', + begin: C_NUMBER_RE, + relevance: 0 + }; + const BINARY_NUMBER_MODE = { + scope: 'number', + begin: BINARY_NUMBER_RE, + relevance: 0 + }; + const REGEXP_MODE = { + // this outer rule makes sure we actually have a WHOLE regex and not simply + // an expression such as: + // + // 3 / something + // + // (which will then blow up when regex's `illegal` sees the newline) + begin: /(?=\/[^/\n]*\/)/, + contains: [{ + scope: 'regexp', + begin: /\//, + end: /\/[gimuy]*/, + illegal: /\n/, + contains: [ + BACKSLASH_ESCAPE, + { + begin: /\[/, + end: /\]/, + relevance: 0, + contains: [BACKSLASH_ESCAPE] + } + ] + }] + }; + const TITLE_MODE = { + scope: 'title', + begin: IDENT_RE$1, + relevance: 0 + }; + const UNDERSCORE_TITLE_MODE = { + scope: 'title', + begin: UNDERSCORE_IDENT_RE, + relevance: 0 + }; + const METHOD_GUARD = { + // excludes method names from keyword processing + begin: '\\.\\s*' + UNDERSCORE_IDENT_RE, + relevance: 0 + }; + + /** + * Adds end same as begin mechanics to a mode + * + * Your mode must include at least a single () match group as that first match + * group is what is used for comparison + * @param {Partial} mode + */ + const END_SAME_AS_BEGIN = function (mode) { + return Object.assign(mode, + { + /** @type {ModeCallback} */ + 'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; }, + /** @type {ModeCallback} */ + 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); } + }); + }; + + var MODES$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + MATCH_NOTHING_RE: MATCH_NOTHING_RE, + IDENT_RE: IDENT_RE$1, + UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE, + NUMBER_RE: NUMBER_RE, + C_NUMBER_RE: C_NUMBER_RE, + BINARY_NUMBER_RE: BINARY_NUMBER_RE, + RE_STARTERS_RE: RE_STARTERS_RE, + SHEBANG: SHEBANG, + BACKSLASH_ESCAPE: BACKSLASH_ESCAPE, + APOS_STRING_MODE: APOS_STRING_MODE, + QUOTE_STRING_MODE: QUOTE_STRING_MODE, + PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE, + COMMENT: COMMENT, + C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE, + C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE, + HASH_COMMENT_MODE: HASH_COMMENT_MODE, + NUMBER_MODE: NUMBER_MODE, + C_NUMBER_MODE: C_NUMBER_MODE, + BINARY_NUMBER_MODE: BINARY_NUMBER_MODE, + REGEXP_MODE: REGEXP_MODE, + TITLE_MODE: TITLE_MODE, + UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE, + METHOD_GUARD: METHOD_GUARD, + END_SAME_AS_BEGIN: END_SAME_AS_BEGIN + }); + + /** + @typedef {import('highlight.js').CallbackResponse} CallbackResponse + @typedef {import('highlight.js').CompilerExt} CompilerExt + */ + + // Grammar extensions / plugins + // See: https://github.com/highlightjs/highlight.js/issues/2833 + + // Grammar extensions allow "syntactic sugar" to be added to the grammar modes + // without requiring any underlying changes to the compiler internals. + + // `compileMatch` being the perfect small example of now allowing a grammar + // author to write `match` when they desire to match a single expression rather + // than being forced to use `begin`. The extension then just moves `match` into + // `begin` when it runs. Ie, no features have been added, but we've just made + // the experience of writing (and reading grammars) a little bit nicer. + + // ------ + + // TODO: We need negative look-behind support to do this properly + /** + * Skip a match if it has a preceding dot + * + * This is used for `beginKeywords` to prevent matching expressions such as + * `bob.keyword.do()`. The mode compiler automatically wires this up as a + * special _internal_ 'on:begin' callback for modes with `beginKeywords` + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + function skipIfHasPrecedingDot(match, response) { + const before = match.input[match.index - 1]; + if (before === ".") { + response.ignoreMatch(); + } + } + + /** + * + * @type {CompilerExt} + */ + function scopeClassName(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.className !== undefined) { + mode.scope = mode.className; + delete mode.className; + } + } + + /** + * `beginKeywords` syntactic sugar + * @type {CompilerExt} + */ + function beginKeywords(mode, parent) { + if (!parent) return; + if (!mode.beginKeywords) return; + + // for languages with keywords that include non-word characters checking for + // a word boundary is not sufficient, so instead we check for a word boundary + // or whitespace - this does no harm in any case since our keyword engine + // doesn't allow spaces in keywords anyways and we still check for the boundary + // first + mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'; + mode.__beforeBegin = skipIfHasPrecedingDot; + mode.keywords = mode.keywords || mode.beginKeywords; + delete mode.beginKeywords; + + // prevents double relevance, the keywords themselves provide + // relevance, the mode doesn't need to double it + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 0; + } + + /** + * Allow `illegal` to contain an array of illegal values + * @type {CompilerExt} + */ + function compileIllegal(mode, _parent) { + if (!Array.isArray(mode.illegal)) return; + + mode.illegal = either(...mode.illegal); + } + + /** + * `match` to match a single expression for readability + * @type {CompilerExt} + */ + function compileMatch(mode, _parent) { + if (!mode.match) return; + if (mode.begin || mode.end) throw new Error("begin & end are not supported with match"); + + mode.begin = mode.match; + delete mode.match; + } + + /** + * provides the default 1 relevance to all modes + * @type {CompilerExt} + */ + function compileRelevance(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 1; + } + + // allow beforeMatch to act as a "qualifier" for the match + // the full match begin must be [beforeMatch][begin] + const beforeMatchExt = (mode, parent) => { + if (!mode.beforeMatch) return; + // starts conflicts with endsParent which we need to make sure the child + // rule is not matched multiple times + if (mode.starts) throw new Error("beforeMatch cannot be used with starts"); + + const originalMode = Object.assign({}, mode); + Object.keys(mode).forEach((key) => { delete mode[key]; }); + + mode.keywords = originalMode.keywords; + mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin)); + mode.starts = { + relevance: 0, + contains: [ + Object.assign(originalMode, { endsParent: true }) + ] + }; + mode.relevance = 0; + + delete originalMode.beforeMatch; + }; + + // keywords that should have no default relevance value + const COMMON_KEYWORDS = [ + 'of', + 'and', + 'for', + 'in', + 'not', + 'or', + 'if', + 'then', + 'parent', // common variable name + 'list', // common variable name + 'value' // common variable name + ]; + + const DEFAULT_KEYWORD_SCOPE = "keyword"; + + /** + * Given raw keywords from a language definition, compile them. + * + * @param {string | Record | Array} rawKeywords + * @param {boolean} caseInsensitive + */ + function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) { + /** @type KeywordDict */ + const compiledKeywords = Object.create(null); + + // input can be a string of keywords, an array of keywords, or a object with + // named keys representing scopeName (which can then point to a string or array) + if (typeof rawKeywords === 'string') { + compileList(scopeName, rawKeywords.split(" ")); + } else if (Array.isArray(rawKeywords)) { + compileList(scopeName, rawKeywords); + } else { + Object.keys(rawKeywords).forEach(function (scopeName) { + // collapse all our objects back into the parent object + Object.assign( + compiledKeywords, + compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName) + ); + }); + } + return compiledKeywords; + + // --- + + /** + * Compiles an individual list of keywords + * + * Ex: "for if when while|5" + * + * @param {string} scopeName + * @param {Array} keywordList + */ + function compileList(scopeName, keywordList) { + if (caseInsensitive) { + keywordList = keywordList.map(x => x.toLowerCase()); + } + keywordList.forEach(function (keyword) { + const pair = keyword.split('|'); + compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])]; + }); + } + } + + /** + * Returns the proper score for a given keyword + * + * Also takes into account comment keywords, which will be scored 0 UNLESS + * another score has been manually assigned. + * @param {string} keyword + * @param {string} [providedScore] + */ + function scoreForKeyword(keyword, providedScore) { + // manual scores always win over common keywords + // so you can force a score of 1 if you really insist + if (providedScore) { + return Number(providedScore); + } + + return commonKeyword(keyword) ? 0 : 1; + } + + /** + * Determines if a given keyword is common or not + * + * @param {string} keyword */ + function commonKeyword(keyword) { + return COMMON_KEYWORDS.includes(keyword.toLowerCase()); + } + + /* + + For the reasoning behind this please see: + https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419 + + */ + + /** + * @type {Record} + */ + const seenDeprecations = {}; + + /** + * @param {string} message + */ + const error = (message) => { + console.error(message); + }; + + /** + * @param {string} message + * @param {any} args + */ + const warn = (message, ...args) => { + console.log(`WARN: ${message}`, ...args); + }; + + /** + * @param {string} version + * @param {string} message + */ + const deprecated = (version, message) => { + if (seenDeprecations[`${version}/${message}`]) return; + + console.log(`Deprecated as of ${version}. ${message}`); + seenDeprecations[`${version}/${message}`] = true; + }; + + /* eslint-disable no-throw-literal */ + + /** + @typedef {import('highlight.js').CompiledMode} CompiledMode + */ + + const MultiClassError = new Error(); + + /** + * Renumbers labeled scope names to account for additional inner match + * groups that otherwise would break everything. + * + * Lets say we 3 match scopes: + * + * { 1 => ..., 2 => ..., 3 => ... } + * + * So what we need is a clean match like this: + * + * (a)(b)(c) => [ "a", "b", "c" ] + * + * But this falls apart with inner match groups: + * + * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ] + * + * Our scopes are now "out of alignment" and we're repeating `b` 3 times. + * What needs to happen is the numbers are remapped: + * + * { 1 => ..., 2 => ..., 5 => ... } + * + * We also need to know that the ONLY groups that should be output + * are 1, 2, and 5. This function handles this behavior. + * + * @param {CompiledMode} mode + * @param {Array} regexes + * @param {{key: "beginScope"|"endScope"}} opts + */ + function remapScopeNames(mode, regexes, { key }) { + let offset = 0; + const scopeNames = mode[key]; + /** @type Record */ + const emit = {}; + /** @type Record */ + const positions = {}; + + for (let i = 1; i <= regexes.length; i++) { + positions[i + offset] = scopeNames[i]; + emit[i + offset] = true; + offset += countMatchGroups(regexes[i - 1]); + } + // we use _emit to keep track of which match groups are "top-level" to avoid double + // output from inside match groups + mode[key] = positions; + mode[key]._emit = emit; + mode[key]._multi = true; + } + + /** + * @param {CompiledMode} mode + */ + function beginMultiClass(mode) { + if (!Array.isArray(mode.begin)) return; + + if (mode.skip || mode.excludeBegin || mode.returnBegin) { + error("skip, excludeBegin, returnBegin not compatible with beginScope: {}"); + throw MultiClassError; + } + + if (typeof mode.beginScope !== "object" || mode.beginScope === null) { + error("beginScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.begin, { key: "beginScope" }); + mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" }); + } + + /** + * @param {CompiledMode} mode + */ + function endMultiClass(mode) { + if (!Array.isArray(mode.end)) return; + + if (mode.skip || mode.excludeEnd || mode.returnEnd) { + error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); + throw MultiClassError; + } + + if (typeof mode.endScope !== "object" || mode.endScope === null) { + error("endScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.end, { key: "endScope" }); + mode.end = _rewriteBackreferences(mode.end, { joinWith: "" }); + } + + /** + * this exists only to allow `scope: {}` to be used beside `match:` + * Otherwise `beginScope` would necessary and that would look weird + + { + match: [ /def/, /\w+/ ] + scope: { 1: "keyword" , 2: "title" } + } + + * @param {CompiledMode} mode + */ + function scopeSugar(mode) { + if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { + mode.beginScope = mode.scope; + delete mode.scope; + } + } + + /** + * @param {CompiledMode} mode + */ + function MultiClass(mode) { + scopeSugar(mode); + + if (typeof mode.beginScope === "string") { + mode.beginScope = { _wrap: mode.beginScope }; + } + if (typeof mode.endScope === "string") { + mode.endScope = { _wrap: mode.endScope }; + } + + beginMultiClass(mode); + endMultiClass(mode); + } + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').CompiledLanguage} CompiledLanguage + */ + + // compilation + + /** + * Compiles a language definition result + * + * Given the raw result of a language definition (Language), compiles this so + * that it is ready for highlighting code. + * @param {Language} language + * @returns {CompiledLanguage} + */ + function compileLanguage(language) { + /** + * Builds a regex with the case sensitivity of the current language + * + * @param {RegExp | string} value + * @param {boolean} [global] + */ + function langRe(value, global) { + return new RegExp( + source(value), + 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') + ); + } + + /** + Stores multiple regular expressions and allows you to quickly search for + them all in a string simultaneously - returning the first match. It does + this by creating a huge (a|b|c) regex - each individual item wrapped with () + and joined by `|` - using match groups to track position. When a match is + found checking which position in the array has content allows us to figure + out which of the original regexes / match groups triggered the match. + + The match object itself (the result of `Regex.exec`) is returned but also + enhanced by merging in any meta-data that was registered with the regex. + This is how we keep track of which mode matched, and what type of rule + (`illegal`, `begin`, end, etc). + */ + class MultiRegex { + constructor() { + this.matchIndexes = {}; + // @ts-ignore + this.regexes = []; + this.matchAt = 1; + this.position = 0; + } + + // @ts-ignore + addRule(re, opts) { + opts.position = this.position++; + // @ts-ignore + this.matchIndexes[this.matchAt] = opts; + this.regexes.push([opts, re]); + this.matchAt += countMatchGroups(re) + 1; + } + + compile() { + if (this.regexes.length === 0) { + // avoids the need to check length every time exec is called + // @ts-ignore + this.exec = () => null; + } + const terminators = this.regexes.map(el => el[1]); + this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: '|' }), true); + this.lastIndex = 0; + } + + /** @param {string} s */ + exec(s) { + this.matcherRe.lastIndex = this.lastIndex; + const match = this.matcherRe.exec(s); + if (!match) { return null; } + + // eslint-disable-next-line no-undefined + const i = match.findIndex((el, i) => i > 0 && el !== undefined); + // @ts-ignore + const matchData = this.matchIndexes[i]; + // trim off any earlier non-relevant match groups (ie, the other regex + // match groups that make up the multi-matcher) + match.splice(0, i); + + return Object.assign(match, matchData); + } + } + + /* + Created to solve the key deficiently with MultiRegex - there is no way to + test for multiple matches at a single location. Why would we need to do + that? In the future a more dynamic engine will allow certain matches to be + ignored. An example: if we matched say the 3rd regex in a large group but + decided to ignore it - we'd need to started testing again at the 4th + regex... but MultiRegex itself gives us no real way to do that. + + So what this class creates MultiRegexs on the fly for whatever search + position they are needed. + + NOTE: These additional MultiRegex objects are created dynamically. For most + grammars most of the time we will never actually need anything more than the + first MultiRegex - so this shouldn't have too much overhead. + + Say this is our search group, and we match regex3, but wish to ignore it. + + regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0 + + What we need is a new MultiRegex that only includes the remaining + possibilities: + + regex4 | regex5 ' ie, startAt = 3 + + This class wraps all that complexity up in a simple API... `startAt` decides + where in the array of expressions to start doing the matching. It + auto-increments, so if a match is found at position 2, then startAt will be + set to 3. If the end is reached startAt will return to 0. + + MOST of the time the parser will be setting startAt manually to 0. + */ + class ResumableMultiRegex { + constructor() { + // @ts-ignore + this.rules = []; + // @ts-ignore + this.multiRegexes = []; + this.count = 0; + + this.lastIndex = 0; + this.regexIndex = 0; + } + + // @ts-ignore + getMatcher(index) { + if (this.multiRegexes[index]) return this.multiRegexes[index]; + + const matcher = new MultiRegex(); + this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts)); + matcher.compile(); + this.multiRegexes[index] = matcher; + return matcher; + } + + resumingScanAtSamePosition() { + return this.regexIndex !== 0; + } + + considerAll() { + this.regexIndex = 0; + } + + // @ts-ignore + addRule(re, opts) { + this.rules.push([re, opts]); + if (opts.type === "begin") this.count++; + } + + /** @param {string} s */ + exec(s) { + const m = this.getMatcher(this.regexIndex); + m.lastIndex = this.lastIndex; + let result = m.exec(s); + + // The following is because we have no easy way to say "resume scanning at the + // existing position but also skip the current rule ONLY". What happens is + // all prior rules are also skipped which can result in matching the wrong + // thing. Example of matching "booger": + + // our matcher is [string, "booger", number] + // + // ....booger.... + + // if "booger" is ignored then we'd really need a regex to scan from the + // SAME position for only: [string, number] but ignoring "booger" (if it + // was the first match), a simple resume would scan ahead who knows how + // far looking only for "number", ignoring potential string matches (or + // future "booger" matches that might be valid.) + + // So what we do: We execute two matchers, one resuming at the same + // position, but the second full matcher starting at the position after: + + // /--- resume first regex match here (for [number]) + // |/---- full match here for [string, "booger", number] + // vv + // ....booger.... + + // Which ever results in a match first is then used. So this 3-4 step + // process essentially allows us to say "match at this position, excluding + // a prior rule that was ignored". + // + // 1. Match "booger" first, ignore. Also proves that [string] does non match. + // 2. Resume matching for [number] + // 3. Match at index + 1 for [string, "booger", number] + // 4. If #2 and #3 result in matches, which came first? + if (this.resumingScanAtSamePosition()) { + if (result && result.index === this.lastIndex); else { // use the second matcher result + const m2 = this.getMatcher(0); + m2.lastIndex = this.lastIndex + 1; + result = m2.exec(s); + } + } + + if (result) { + this.regexIndex += result.position + 1; + if (this.regexIndex === this.count) { + // wrap-around to considering all matches again + this.considerAll(); + } + } + + return result; + } + } + + /** + * Given a mode, builds a huge ResumableMultiRegex that can be used to walk + * the content and find matches. + * + * @param {CompiledMode} mode + * @returns {ResumableMultiRegex} + */ + function buildModeRegex(mode) { + const mm = new ResumableMultiRegex(); + + mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" })); + + if (mode.terminatorEnd) { + mm.addRule(mode.terminatorEnd, { type: "end" }); + } + if (mode.illegal) { + mm.addRule(mode.illegal, { type: "illegal" }); + } + + return mm; + } + + /** skip vs abort vs ignore + * + * @skip - The mode is still entered and exited normally (and contains rules apply), + * but all content is held and added to the parent buffer rather than being + * output when the mode ends. Mostly used with `sublanguage` to build up + * a single large buffer than can be parsed by sublanguage. + * + * - The mode begin ands ends normally. + * - Content matched is added to the parent mode buffer. + * - The parser cursor is moved forward normally. + * + * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it + * never matched) but DOES NOT continue to match subsequent `contains` + * modes. Abort is bad/suboptimal because it can result in modes + * farther down not getting applied because an earlier rule eats the + * content but then aborts. + * + * - The mode does not begin. + * - Content matched by `begin` is added to the mode buffer. + * - The parser cursor is moved forward accordingly. + * + * @ignore - Ignores the mode (as if it never matched) and continues to match any + * subsequent `contains` modes. Ignore isn't technically possible with + * the current parser implementation. + * + * - The mode does not begin. + * - Content matched by `begin` is ignored. + * - The parser cursor is not moved forward. + */ + + /** + * Compiles an individual mode + * + * This can raise an error if the mode contains certain detectable known logic + * issues. + * @param {Mode} mode + * @param {CompiledMode | null} [parent] + * @returns {CompiledMode | never} + */ + function compileMode(mode, parent) { + const cmode = /** @type CompiledMode */ (mode); + if (mode.isCompiled) return cmode; + + [ + scopeClassName, + // do this early so compiler extensions generally don't have to worry about + // the distinction between match/begin + compileMatch, + MultiClass, + beforeMatchExt + ].forEach(ext => ext(mode, parent)); + + language.compilerExtensions.forEach(ext => ext(mode, parent)); + + // __beforeBegin is considered private API, internal use only + mode.__beforeBegin = null; + + [ + beginKeywords, + // do this later so compiler extensions that come earlier have access to the + // raw array if they wanted to perhaps manipulate it, etc. + compileIllegal, + // default to 1 relevance if not specified + compileRelevance + ].forEach(ext => ext(mode, parent)); + + mode.isCompiled = true; + + let keywordPattern = null; + if (typeof mode.keywords === "object" && mode.keywords.$pattern) { + // we need a copy because keywords might be compiled multiple times + // so we can't go deleting $pattern from the original on the first + // pass + mode.keywords = Object.assign({}, mode.keywords); + keywordPattern = mode.keywords.$pattern; + delete mode.keywords.$pattern; + } + keywordPattern = keywordPattern || /\w+/; + + if (mode.keywords) { + mode.keywords = compileKeywords(mode.keywords, language.case_insensitive); + } + + cmode.keywordPatternRe = langRe(keywordPattern, true); + + if (parent) { + if (!mode.begin) mode.begin = /\B|\b/; + cmode.beginRe = langRe(mode.begin); + if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; + if (mode.end) cmode.endRe = langRe(mode.end); + cmode.terminatorEnd = source(mode.end) || ''; + if (mode.endsWithParent && parent.terminatorEnd) { + cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd; + } + } + if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */(mode.illegal)); + if (!mode.contains) mode.contains = []; + + mode.contains = [].concat(...mode.contains.map(function (c) { + return expandOrCloneMode(c === 'self' ? mode : c); + })); + mode.contains.forEach(function (c) { compileMode(/** @type Mode */(c), cmode); }); + + if (mode.starts) { + compileMode(mode.starts, parent); + } + + cmode.matcher = buildModeRegex(cmode); + return cmode; + } + + if (!language.compilerExtensions) language.compilerExtensions = []; + + // self is not valid at the top-level + if (language.contains && language.contains.includes('self')) { + throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation."); + } + + // we need a null object, which inherit will guarantee + language.classNameAliases = inherit$1(language.classNameAliases || {}); + + return compileMode(/** @type Mode */(language)); + } + + /** + * Determines if a mode has a dependency on it's parent or not + * + * If a mode does have a parent dependency then often we need to clone it if + * it's used in multiple places so that each copy points to the correct parent, + * where-as modes without a parent can often safely be re-used at the bottom of + * a mode chain. + * + * @param {Mode | null} mode + * @returns {boolean} - is there a dependency on the parent? + * */ + function dependencyOnParent(mode) { + if (!mode) return false; + + return mode.endsWithParent || dependencyOnParent(mode.starts); + } + + /** + * Expands a mode or clones it if necessary + * + * This is necessary for modes with parental dependenceis (see notes on + * `dependencyOnParent`) and for nodes that have `variants` - which must then be + * exploded into their own individual modes at compile time. + * + * @param {Mode} mode + * @returns {Mode | Mode[]} + * */ + function expandOrCloneMode(mode) { + if (mode.variants && !mode.cachedVariants) { + mode.cachedVariants = mode.variants.map(function (variant) { + return inherit$1(mode, { variants: null }, variant); + }); + } + + // EXPAND + // if we have variants then essentially "replace" the mode with the variants + // this happens in compileMode, where this function is called from + if (mode.cachedVariants) { + return mode.cachedVariants; + } + + // CLONE + // if we have dependencies on parents then we need a unique + // instance of ourselves, so we can be reused with many + // different parents without issue + if (dependencyOnParent(mode)) { + return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null }); + } + + if (Object.isFrozen(mode)) { + return inherit$1(mode); + } + + // no special dependency issues, just return ourselves + return mode; + } + + var version = "11.0.0-beta1"; + + /* + Syntax highlighting with language autodetection. + https://highlightjs.org/ + */ + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSApi} HLJSApi + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').PluginEvent} PluginEvent + @typedef {import('highlight.js').HLJSOptions} HLJSOptions + @typedef {import('highlight.js').LanguageFn} LanguageFn + @typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement + @typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext + @typedef {import('highlight.js/private').MatchType} MatchType + @typedef {import('highlight.js/private').KeywordData} KeywordData + @typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch + @typedef {import('highlight.js/private').AnnotatedError} AnnotatedError + @typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult + @typedef {import('highlight.js').HighlightOptions} HighlightOptions + @typedef {import('highlight.js').HighlightResult} HighlightResult + */ + + + const escape = escapeHTML; + const inherit = inherit$1; + const NO_MATCH = Symbol("nomatch"); + const MAX_KEYWORD_HITS = 7; + + /** + * @param {any} hljs - object that is extended (legacy) + * @returns {HLJSApi} + */ + const HLJS = function (hljs) { + // Global internal variables used within the highlight.js library. + /** @type {Record} */ + const languages = Object.create(null); + /** @type {Record} */ + const aliases = Object.create(null); + /** @type {HLJSPlugin[]} */ + const plugins = []; + + // safe/production mode - swallows more errors, tries to keep running + // even if a single syntax or parse hits a fatal error + let SAFE_MODE = true; + const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?"; + /** @type {Language} */ + const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] }; + + // Global options used when within external APIs. This is modified when + // calling the `hljs.configure` function. + /** @type HLJSOptions */ + let options = { + ignoreUnescapedHTML: false, + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: 'hljs-', + cssSelector: 'pre code', + languages: null, + // beta configuration options, subject to change, welcome to discuss + // https://github.com/highlightjs/highlight.js/issues/1086 + __emitter: TokenTreeEmitter + }; + + /* Utility functions */ + + /** + * Tests a language name to see if highlighting should be skipped + * @param {string} languageName + */ + function shouldNotHighlight(languageName) { + return options.noHighlightRe.test(languageName); + } + + /** + * @param {HighlightedHTMLElement} block - the HTML element to determine language for + */ + function blockLanguage(block) { + let classes = block.className + ' '; + + classes += block.parentNode ? block.parentNode.className : ''; + + // language-* takes precedence over non-prefixed class names. + const match = options.languageDetectRe.exec(classes); + if (match) { + const language = getLanguage(match[1]); + if (!language) { + warn(LANGUAGE_NOT_FOUND.replace("{}", match[1])); + warn("Falling back to no-highlight mode for this block.", block); + } + return language ? match[1] : 'no-highlight'; + } + + return classes + .split(/\s+/) + .find((_class) => shouldNotHighlight(_class) || getLanguage(_class)); + } + + /** + * Core highlighting function. + * + * OLD API + * highlight(lang, code, ignoreIllegals, continuation) + * + * NEW API + * highlight(code, {lang, ignoreIllegals}) + * + * @param {string} codeOrLanguageName - the language to use for highlighting + * @param {string | HighlightOptions} optionsOrCode - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode} [continuation] - current continuation mode, if any + * + * @returns {HighlightResult} Result - an object that represents the result + * @property {string} language - the language name + * @property {number} relevance - the relevance score + * @property {string} value - the highlighted HTML code + * @property {string} code - the original raw code + * @property {CompiledMode} top - top of the current mode stack + * @property {boolean} illegal - indicates whether any illegal matches were found + */ + function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals, continuation) { + let code = ""; + let languageName = ""; + if (typeof optionsOrCode === "object") { + code = codeOrLanguageName; + ignoreIllegals = optionsOrCode.ignoreIllegals; + languageName = optionsOrCode.language; + // continuation not supported at all via the new API + // eslint-disable-next-line no-undefined + continuation = undefined; + } else { + // old API + deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated."); + deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"); + languageName = codeOrLanguageName; + code = optionsOrCode; + } + + // https://github.com/highlightjs/highlight.js/issues/3149 + // eslint-disable-next-line no-undefined + if (ignoreIllegals === undefined) { ignoreIllegals = true; } + + /** @type {BeforeHighlightContext} */ + const context = { + code, + language: languageName + }; + // the plugin can change the desired language or the code to be highlighted + // just be changing the object it was passed + fire("before:highlight", context); + + // a before plugin can usurp the result completely by providing it's own + // in which case we don't even need to call highlight + const result = context.result + ? context.result + : _highlight(context.language, context.code, ignoreIllegals, continuation); + + result.code = context.code; + // the plugin can change anything in result to suite it + fire("after:highlight", result); + + return result; + } + + /** + * private highlight that's used internally and does not fire callbacks + * + * @param {string} languageName - the language to use for highlighting + * @param {string} codeToHighlight - the code to highlight + * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode?} [continuation] - current continuation mode, if any + * @returns {HighlightResult} - result of the highlight operation + */ + function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) { + const keywordHits = Object.create(null); + + /** + * Return keyword data if a match is a keyword + * @param {CompiledMode} mode - current mode + * @param {string} matchText - the textual match + * @returns {KeywordData | false} + */ + function keywordData(mode, matchText) { + return mode.keywords[matchText]; + } + + function processKeywords() { + if (!top.keywords) { + emitter.addText(modeBuffer); + return; + } + + let lastIndex = 0; + top.keywordPatternRe.lastIndex = 0; + let match = top.keywordPatternRe.exec(modeBuffer); + let buf = ""; + + while (match) { + buf += modeBuffer.substring(lastIndex, match.index); + const word = language.case_insensitive ? match[0].toLowerCase() : match[0]; + const data = keywordData(top, word); + if (data) { + const [kind, keywordRelevance] = data; + emitter.addText(buf); + buf = ""; + + keywordHits[word] = (keywordHits[word] || 0) + 1; + if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance; + if (kind.startsWith("_")) { + // _ implied for relevance only, do not highlight + // by applying a class name + buf += match[0]; + } else { + const cssClass = language.classNameAliases[kind] || kind; + emitter.addKeyword(match[0], cssClass); + } + } else { + buf += match[0]; + } + lastIndex = top.keywordPatternRe.lastIndex; + match = top.keywordPatternRe.exec(modeBuffer); + } + buf += modeBuffer.substr(lastIndex); + emitter.addText(buf); + } + + function processSubLanguage() { + if (modeBuffer === "") return; + /** @type HighlightResult */ + let result = null; + + if (typeof top.subLanguage === 'string') { + if (!languages[top.subLanguage]) { + emitter.addText(modeBuffer); + return; + } + result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]); + continuations[top.subLanguage] = /** @type {CompiledMode} */ (result._top); + } else { + result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null); + } + + // Counting embedded language score towards the host language may be disabled + // with zeroing the containing mode relevance. Use case in point is Markdown that + // allows XML everywhere and makes every XML snippet to have a much larger Markdown + // score. + if (top.relevance > 0) { + relevance += result.relevance; + } + emitter.addSublanguage(result._emitter, result.language); + } + + function processBuffer() { + if (top.subLanguage != null) { + processSubLanguage(); + } else { + processKeywords(); + } + modeBuffer = ''; + } + + /** + * @param {CompiledMode} mode + * @param {RegExpMatchArray} match + */ + function emitMultiClass(scope, match) { + let i = 1; + // eslint-disable-next-line no-undefined + while (match[i] !== undefined) { + if (!scope._emit[i]) { i++; continue; } + const klass = language.classNameAliases[scope[i]] || scope[i]; + const text = match[i]; + if (klass) { + emitter.addKeyword(text, klass); + } else { + modeBuffer = text; + processKeywords(); + modeBuffer = ""; + } + i++; + } + } + + /** + * @param {CompiledMode} mode - new mode to start + * @param {RegExpMatchArray} match + */ + function startNewMode(mode, match) { + if (mode.scope && typeof mode.scope === "string") { + emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); + } + if (mode.beginScope) { + // beginScope just wraps the begin match itself in a scope + if (mode.beginScope._wrap) { + emitter.addKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap); + modeBuffer = ""; + } else if (mode.beginScope._multi) { + // at this point modeBuffer should just be the match + emitMultiClass(mode.beginScope, match); + modeBuffer = ""; + } + } + + top = Object.create(mode, { parent: { value: top } }); + return top; + } + + /** + * @param {CompiledMode } mode - the mode to potentially end + * @param {RegExpMatchArray} match - the latest match + * @param {string} matchPlusRemainder - match plus remainder of content + * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode + */ + function endOfMode(mode, match, matchPlusRemainder) { + let matched = startsWith(mode.endRe, matchPlusRemainder); + + if (matched) { + if (mode["on:end"]) { + const resp = new Response(mode); + mode["on:end"](match, resp); + if (resp.isMatchIgnored) matched = false; + } + + if (matched) { + while (mode.endsParent && mode.parent) { + mode = mode.parent; + } + return mode; + } + } + // even if on:end fires an `ignore` it's still possible + // that we might trigger the end node because of a parent mode + if (mode.endsWithParent) { + return endOfMode(mode.parent, match, matchPlusRemainder); + } + } + + /** + * Handle matching but then ignoring a sequence of text + * + * @param {string} lexeme - string containing full match text + */ + function doIgnore(lexeme) { + if (top.matcher.regexIndex === 0) { + // no more regexes to potentially match here, so we move the cursor forward one + // space + modeBuffer += lexeme[0]; + return 1; + } else { + // no need to move the cursor, we still have additional regexes to try and + // match at this very spot + resumeScanAtSamePosition = true; + return 0; + } + } + + /** + * Handle the start of a new potential mode match + * + * @param {EnhancedMatch} match - the current match + * @returns {number} how far to advance the parse cursor + */ + function doBeginMatch(match) { + const lexeme = match[0]; + const newMode = match.rule; + + const resp = new Response(newMode); + // first internal before callbacks, then the public ones + const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]]; + for (const cb of beforeCallbacks) { + if (!cb) continue; + cb(match, resp); + if (resp.isMatchIgnored) return doIgnore(lexeme); + } + + if (newMode.skip) { + modeBuffer += lexeme; + } else { + if (newMode.excludeBegin) { + modeBuffer += lexeme; + } + processBuffer(); + if (!newMode.returnBegin && !newMode.excludeBegin) { + modeBuffer = lexeme; + } + } + startNewMode(newMode, match); + return newMode.returnBegin ? 0 : lexeme.length; + } + + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ + function doEndMatch(match) { + const lexeme = match[0]; + const matchPlusRemainder = codeToHighlight.substr(match.index); + + const endMode = endOfMode(top, match, matchPlusRemainder); + if (!endMode) { return NO_MATCH; } + + const origin = top; + if (top.endScope && top.endScope._wrap) { + processBuffer(); + emitter.addKeyword(lexeme, top.endScope._wrap); + } else if (top.endScope && top.endScope._multi) { + processBuffer(); + emitMultiClass(top.endScope, match); + } else if (origin.skip) { + modeBuffer += lexeme; + } else { + if (!(origin.returnEnd || origin.excludeEnd)) { + modeBuffer += lexeme; + } + processBuffer(); + if (origin.excludeEnd) { + modeBuffer = lexeme; + } + } + do { + if (top.scope && !top.isMultiClass) { + emitter.closeNode(); + } + if (!top.skip && !top.subLanguage) { + relevance += top.relevance; + } + top = top.parent; + } while (top !== endMode.parent); + if (endMode.starts) { + startNewMode(endMode.starts, match); + } + return origin.returnEnd ? 0 : lexeme.length; + } + + function processContinuations() { + const list = []; + for (let current = top; current !== language; current = current.parent) { + if (current.scope) { + list.unshift(current.scope); + } + } + list.forEach(item => emitter.openNode(item)); + } + + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ + let lastMatch = {}; + + /** + * Process an individual match + * + * @param {string} textBeforeMatch - text preceding the match (since the last match) + * @param {EnhancedMatch} [match] - the match itself + */ + function processLexeme(textBeforeMatch, match) { + const lexeme = match && match[0]; + + // add non-matched text to the current mode buffer + modeBuffer += textBeforeMatch; + + if (lexeme == null) { + processBuffer(); + return 0; + } + + // we've found a 0 width match and we're stuck, so we need to advance + // this happens when we have badly behaved rules that have optional matchers to the degree that + // sometimes they can end up matching nothing at all + // Ref: https://github.com/highlightjs/highlight.js/issues/2140 + if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") { + // spit the "skipped" character that our regex choked on back into the output sequence + modeBuffer += codeToHighlight.slice(match.index, match.index + 1); + if (!SAFE_MODE) { + /** @type {AnnotatedError} */ + const err = new Error(`0 width match regex (${languageName})`); + err.languageName = languageName; + err.badRule = lastMatch.rule; + throw err; + } + return 1; + } + lastMatch = match; + + if (match.type === "begin") { + return doBeginMatch(match); + } else if (match.type === "illegal" && !ignoreIllegals) { + // illegal match, we do not continue processing + /** @type {AnnotatedError} */ + const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.scope || '') + '"'); + err.mode = top; + throw err; + } else if (match.type === "end") { + const processed = doEndMatch(match); + if (processed !== NO_MATCH) { + return processed; + } + } + + // edge case for when illegal matches $ (end of line) which is technically + // a 0 width match but not a begin/end match so it's not caught by the + // first handler (when ignoreIllegals is true) + if (match.type === "illegal" && lexeme === "") { + // advance so we aren't stuck in an infinite loop + return 1; + } + + // infinite loops are BAD, this is a last ditch catch all. if we have a + // decent number of iterations yet our index (cursor position in our + // parsing) still 3x behind our index then something is very wrong + // so we bail + if (iterations > 100000 && iterations > match.index * 3) { + const err = new Error('potential infinite loop, way more iterations than matches'); + throw err; + } + + /* + Why might be find ourselves here? An potential end match that was + triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH. + (this could be because a callback requests the match be ignored, etc) + + This causes no real harm other than stopping a few times too many. + */ + + modeBuffer += lexeme; + return lexeme.length; + } + + const language = getLanguage(languageName); + if (!language) { + error(LANGUAGE_NOT_FOUND.replace("{}", languageName)); + throw new Error('Unknown language: "' + languageName + '"'); + } + + const md = compileLanguage(language); + let result = ''; + /** @type {CompiledMode} */ + let top = continuation || md; + /** @type Record */ + const continuations = {}; // keep continuations for sub-languages + const emitter = new options.__emitter(options); + processContinuations(); + let modeBuffer = ''; + let relevance = 0; + let index = 0; + let iterations = 0; + let resumeScanAtSamePosition = false; + + try { + top.matcher.considerAll(); + + for (; ;) { + iterations++; + if (resumeScanAtSamePosition) { + // only regexes not matched previously will now be + // considered for a potential match + resumeScanAtSamePosition = false; + } else { + top.matcher.considerAll(); + } + top.matcher.lastIndex = index; + + const match = top.matcher.exec(codeToHighlight); + // console.log("match", match[0], match.rule && match.rule.begin) + + if (!match) break; + + const beforeMatch = codeToHighlight.substring(index, match.index); + const processedCount = processLexeme(beforeMatch, match); + index = match.index + processedCount; + } + processLexeme(codeToHighlight.substr(index)); + emitter.closeAllNodes(); + emitter.finalize(); + result = emitter.toHTML(); + + return { + language: languageName, + value: result, + relevance: relevance, + illegal: false, + _emitter: emitter, + _top: top + }; + } catch (err) { + if (err.message && err.message.includes('Illegal')) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: true, + relevance: 0, + _illegalBy: { + message: err.message, + index: index, + context: codeToHighlight.slice(index - 100, index + 100), + mode: err.mode, + resultSoFar: result + }, + _emitter: emitter + }; + } else if (SAFE_MODE) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: false, + relevance: 0, + errorRaised: err, + _emitter: emitter, + _top: top + }; + } else { + throw err; + } + } + } + + /** + * returns a valid highlight result, without actually doing any actual work, + * auto highlight starts with this and it's possible for small snippets that + * auto-detection may not find a better match + * @param {string} code + * @returns {HighlightResult} + */ + function justTextHighlightResult(code) { + const result = { + value: escape(code), + illegal: false, + relevance: 0, + _top: PLAINTEXT_LANGUAGE, + _emitter: new options.__emitter(options) + }; + result._emitter.addText(code); + return result; + } + + /** + Highlighting with language detection. Accepts a string with the code to + highlight. Returns an object with the following properties: + + - language (detected language) + - relevance (int) + - value (an HTML string with highlighting markup) + - secondBest (object with the same structure for second-best heuristically + detected language, may be absent) + + @param {string} code + @param {Array} [languageSubset] + @returns {AutoHighlightResult} + */ + function highlightAuto(code, languageSubset) { + languageSubset = languageSubset || options.languages || Object.keys(languages); + const plaintext = justTextHighlightResult(code); + + const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name => + _highlight(name, code, false) + ); + results.unshift(plaintext); // plaintext is always an option + + const sorted = results.sort((a, b) => { + // sort base on relevance + if (a.relevance !== b.relevance) return b.relevance - a.relevance; + + // always award the tie to the base language + // ie if C++ and Arduino are tied, it's more likely to be C++ + if (a.language && b.language) { + if (getLanguage(a.language).supersetOf === b.language) { + return 1; + } else if (getLanguage(b.language).supersetOf === a.language) { + return -1; + } + } + + // otherwise say they are equal, which has the effect of sorting on + // relevance while preserving the original ordering - which is how ties + // have historically been settled, ie the language that comes first always + // wins in the case of a tie + return 0; + }); + + const [best, secondBest] = sorted; + + /** @type {AutoHighlightResult} */ + const result = best; + result.secondBest = secondBest; + + return result; + } + + /** + * Builds new class name for block given the language name + * + * @param {HTMLElement} element + * @param {string} [currentLang] + * @param {string} [resultLang] + */ + function updateClassName(element, currentLang, resultLang) { + const language = (currentLang && aliases[currentLang]) || resultLang; + + element.classList.add("hljs"); + element.classList.add(`language-${language}`); + } + + /** + * Applies highlighting to a DOM node containing code. + * + * @param {HighlightedHTMLElement} element - the HTML element to highlight + */ + function highlightElement(element) { + /** @type HTMLElement */ + let node = null; + const language = blockLanguage(element); + + if (shouldNotHighlight(language)) return; + + fire("before:highlightElement", + { el: element, language: language }); + + // we should be all text, no child nodes + if (!options.ignoreUnescapedHTML && element.children.length > 0) { + console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."); + console.warn("https://github.com/highlightjs/highlight.js/issues/2886"); + console.warn(element); + } + + node = element; + const text = node.textContent; + const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text); + + fire("after:highlightElement", { el: element, result, text }); + + element.innerHTML = result.value; + updateClassName(element, language, result.language); + element.result = { + language: result.language, + // TODO: remove with version 11.0 + re: result.relevance, + relevance: result.relevance + }; + if (result.secondBest) { + element.secondBest = { + language: result.secondBest.language, + relevance: result.secondBest.relevance + }; + } + } + + /** + * Updates highlight.js global options with the passed options + * + * @param {Partial} userOptions + */ + function configure(userOptions) { + options = inherit(options, userOptions); + } + + // TODO: remove v12, deprecated + const initHighlighting = () => { + highlightAll(); + deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now."); + }; + + // TODO: remove v12, deprecated + function initHighlightingOnLoad() { + highlightAll(); + deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now."); + } + + let wantsHighlight = false; + + /** + * auto-highlights all pre>code elements on the page + */ + function highlightAll() { + // if we are called too early in the loading process + if (document.readyState === "loading") { + wantsHighlight = true; + return; + } + + const blocks = document.querySelectorAll(options.cssSelector); + blocks.forEach(highlightElement); + } + + function boot() { + // if a highlight was requested before DOM was loaded, do now + if (wantsHighlight) highlightAll(); + } + + // make sure we are in the browser environment + if (typeof window !== 'undefined' && window.addEventListener) { + window.addEventListener('DOMContentLoaded', boot, false); + } + + /** + * Register a language grammar module + * + * @param {string} languageName + * @param {LanguageFn} languageDefinition + */ + function registerLanguage(languageName, languageDefinition) { + let lang = null; + try { + lang = languageDefinition(hljs); + } catch (error$1) { + error("Language definition for '{}' could not be registered.".replace("{}", languageName)); + // hard or soft error + if (!SAFE_MODE) { throw error$1; } else { error(error$1); } + // languages that have serious errors are replaced with essentially a + // "plaintext" stand-in so that the code blocks will still get normal + // css classes applied to them - and one bad language won't break the + // entire highlighter + lang = PLAINTEXT_LANGUAGE; + } + // give it a temporary name if it doesn't have one in the meta-data + if (!lang.name) lang.name = languageName; + languages[languageName] = lang; + lang.rawDefinition = languageDefinition.bind(null, hljs); + + if (lang.aliases) { + registerAliases(lang.aliases, { languageName }); + } + } + + /** + * Remove a language grammar module + * + * @param {string} languageName + */ + function unregisterLanguage(languageName) { + delete languages[languageName]; + for (const alias of Object.keys(aliases)) { + if (aliases[alias] === languageName) { + delete aliases[alias]; + } + } + } + + /** + * @returns {string[]} List of language internal names + */ + function listLanguages() { + return Object.keys(languages); + } + + /** + * @param {string} name - name of the language to retrieve + * @returns {Language | undefined} + */ + function getLanguage(name) { + name = (name || '').toLowerCase(); + return languages[name] || languages[aliases[name]]; + } + + /** + * + * @param {string|string[]} aliasList - single alias or list of aliases + * @param {{languageName: string}} opts + */ + function registerAliases(aliasList, { languageName }) { + if (typeof aliasList === 'string') { + aliasList = [aliasList]; + } + aliasList.forEach(alias => { aliases[alias.toLowerCase()] = languageName; }); + } + + /** + * Determines if a given language has auto-detection enabled + * @param {string} name - name of the language + */ + function autoDetection(name) { + const lang = getLanguage(name); + return lang && !lang.disableAutodetect; + } + + /** + * Upgrades the old highlightBlock plugins to the new + * highlightElement API + * @param {HLJSPlugin} plugin + */ + function upgradePluginAPI(plugin) { + // TODO: remove with v12 + if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) { + plugin["before:highlightElement"] = (data) => { + plugin["before:highlightBlock"]( + Object.assign({ block: data.el }, data) + ); + }; + } + if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) { + plugin["after:highlightElement"] = (data) => { + plugin["after:highlightBlock"]( + Object.assign({ block: data.el }, data) + ); + }; + } + } + + /** + * @param {HLJSPlugin} plugin + */ + function addPlugin(plugin) { + upgradePluginAPI(plugin); + plugins.push(plugin); + } + + /** + * + * @param {PluginEvent} event + * @param {any} args + */ + function fire(event, args) { + const cb = event; + plugins.forEach(function (plugin) { + if (plugin[cb]) { + plugin[cb](args); + } + }); + } + + /** + * + * @param {HighlightedHTMLElement} el + */ + function deprecateHighlightBlock(el) { + deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0"); + deprecated("10.7.0", "Please use highlightElement now."); + + return highlightElement(el); + } + + /* Interface definition */ + Object.assign(hljs, { + highlight, + highlightAuto, + highlightAll, + highlightElement, + // TODO: Remove with v12 API + highlightBlock: deprecateHighlightBlock, + configure, + initHighlighting, + initHighlightingOnLoad, + registerLanguage, + unregisterLanguage, + listLanguages, + getLanguage, + registerAliases, + autoDetection, + inherit, + addPlugin + }); + + hljs.debugMode = function () { SAFE_MODE = false; }; + hljs.safeMode = function () { SAFE_MODE = true; }; + hljs.versionString = version; + + for (const key in MODES$1) { + // @ts-ignore + if (typeof MODES$1[key] === "object") { + // @ts-ignore + deepFreeze$1(MODES$1[key]); + } + } + + // merge all the modes/regexes into our main object + Object.assign(hljs, MODES$1); + + return hljs; + }; + + // export an "instance" of the highlighter + var HighlightJS = HLJS({}); + + /* + Language: Bash + Author: vah + Contributrors: Benjamin Pannell + Website: https://www.gnu.org/software/bash/ + Category: common + */ + + /** @type LanguageFn */ + function bash(hljs) { + const VAR = {}; + const BRACED_VAR = { + begin: /\$\{/, + end: /\}/, + contains: [ + "self", + { + begin: /:-/, + contains: [VAR] + } // default values + ] + }; + Object.assign(VAR, { + className: 'variable', + variants: [ + { + begin: concat(/\$[\w\d#@][\w\d_]*/, + // negative look-ahead tries to avoid matching patterns that are not + // Perl at all like $ident$, @ident@, etc. + `(?![\\w\\d])(?![$])`) + }, + BRACED_VAR + ] + }); + + const SUBST = { + className: 'subst', + begin: /\$\(/, end: /\)/, + contains: [hljs.BACKSLASH_ESCAPE] + }; + const HERE_DOC = { + begin: /<<-?\s*(?=\w+)/, + starts: { + contains: [ + hljs.END_SAME_AS_BEGIN({ + begin: /(\w+)/, + end: /(\w+)/, + className: 'string' + }) + ] + } + }; + const QUOTE_STRING = { + className: 'string', + begin: /"/, end: /"/, + contains: [ + hljs.BACKSLASH_ESCAPE, + VAR, + SUBST + ] + }; + SUBST.contains.push(QUOTE_STRING); + const ESCAPED_QUOTE = { + className: '', + begin: /\\"/ + + }; + const APOS_STRING = { + className: 'string', + begin: /'/, end: /'/ + }; + const ARITHMETIC = { + begin: /\$\(\(/, + end: /\)\)/, + contains: [ + { begin: /\d+#[0-9a-f]+/, className: "number" }, + hljs.NUMBER_MODE, + VAR + ] + }; + const SH_LIKE_SHELLS = [ + "fish", + "bash", + "zsh", + "sh", + "csh", + "ksh", + "tcsh", + "dash", + "scsh", + ]; + const KNOWN_SHEBANG = hljs.SHEBANG({ + binary: `(${SH_LIKE_SHELLS.join("|")})`, + relevance: 10 + }); + const FUNCTION = { + className: 'function', + begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, + returnBegin: true, + contains: [hljs.inherit(hljs.TITLE_MODE, { begin: /\w[\w\d_]*/ })], + relevance: 0 + }; + + const KEYWORDS = [ + "if", + "then", + "else", + "elif", + "fi", + "for", + "while", + "in", + "do", + "done", + "case", + "esac", + "function" + ]; + + const LITERALS = [ + "true", + "false" + ]; + + return { + name: 'Bash', + aliases: ['sh'], + keywords: { + $pattern: /\b[a-z._-]+\b/, + keyword: KEYWORDS, + literal: LITERALS, + built_in: + // Shell built-ins + // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html + 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times ' + + 'trap umask unset ' + + // Bash built-ins + 'alias bind builtin caller command declare echo enable help let local logout mapfile printf ' + + 'read readarray source type typeset ulimit unalias ' + + // Shell modifiers + 'set shopt ' + + // Zsh built-ins + 'autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles ' + + 'compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate ' + + 'fc fg float functions getcap getln history integer jobs kill limit log noglob popd print ' + + 'pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit ' + + 'unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof ' + + 'zpty zregexparse zsocket zstyle ztcp' + }, + contains: [ + KNOWN_SHEBANG, // to catch known shells and boost relevancy + hljs.SHEBANG(), // to catch unknown shells but still highlight the shebang + FUNCTION, + ARITHMETIC, + hljs.HASH_COMMENT_MODE, + HERE_DOC, + QUOTE_STRING, + ESCAPED_QUOTE, + APOS_STRING, + VAR + ] + }; + } + + + const MODES = (hljs) => { + return { + IMPORTANT: { + scope: 'meta', + begin: '!important' + }, + HEXCOLOR: { + scope: 'number', + begin: '#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})' + }, + ATTRIBUTE_SELECTOR_MODE: { + scope: 'selector-attr', + begin: /\[/, + end: /\]/, + illegal: '$', + contains: [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE + ] + }, + CSS_NUMBER_MODE: { + scope: 'number', + begin: hljs.NUMBER_RE + '(' + + '%|em|ex|ch|rem' + + '|vw|vh|vmin|vmax' + + '|cm|mm|in|pt|pc|px' + + '|deg|grad|rad|turn' + + '|s|ms' + + '|Hz|kHz' + + '|dpi|dpcm|dppx' + + ')?', + relevance: 0 + } + }; + }; + + const TAGS = [ + 'a', + 'abbr', + 'address', + 'article', + 'aside', + 'audio', + 'b', + 'blockquote', + 'body', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'dd', + 'del', + 'details', + 'dfn', + 'div', + 'dl', + 'dt', + 'em', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'main', + 'mark', + 'menu', + 'nav', + 'object', + 'ol', + 'p', + 'q', + 'quote', + 'samp', + 'section', + 'span', + 'strong', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'ul', + 'var', + 'video' + ]; + + const MEDIA_FEATURES = [ + 'any-hover', + 'any-pointer', + 'aspect-ratio', + 'color', + 'color-gamut', + 'color-index', + 'device-aspect-ratio', + 'device-height', + 'device-width', + 'display-mode', + 'forced-colors', + 'grid', + 'height', + 'hover', + 'inverted-colors', + 'monochrome', + 'orientation', + 'overflow-block', + 'overflow-inline', + 'pointer', + 'prefers-color-scheme', + 'prefers-contrast', + 'prefers-reduced-motion', + 'prefers-reduced-transparency', + 'resolution', + 'scan', + 'scripting', + 'update', + 'width', + // TODO: find a better solution? + 'min-width', + 'max-width', + 'min-height', + 'max-height' + ]; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes + const PSEUDO_CLASSES = [ + 'active', + 'any-link', + 'blank', + 'checked', + 'current', + 'default', + 'defined', + 'dir', // dir() + 'disabled', + 'drop', + 'empty', + 'enabled', + 'first', + 'first-child', + 'first-of-type', + 'fullscreen', + 'future', + 'focus', + 'focus-visible', + 'focus-within', + 'has', // has() + 'host', // host or host() + 'host-context', // host-context() + 'hover', + 'indeterminate', + 'in-range', + 'invalid', + 'is', // is() + 'lang', // lang() + 'last-child', + 'last-of-type', + 'left', + 'link', + 'local-link', + 'not', // not() + 'nth-child', // nth-child() + 'nth-col', // nth-col() + 'nth-last-child', // nth-last-child() + 'nth-last-col', // nth-last-col() + 'nth-last-of-type', //nth-last-of-type() + 'nth-of-type', //nth-of-type() + 'only-child', + 'only-of-type', + 'optional', + 'out-of-range', + 'past', + 'placeholder-shown', + 'read-only', + 'read-write', + 'required', + 'right', + 'root', + 'scope', + 'target', + 'target-within', + 'user-invalid', + 'valid', + 'visited', + 'where' // where() + ]; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements + const PSEUDO_ELEMENTS = [ + 'after', + 'backdrop', + 'before', + 'cue', + 'cue-region', + 'first-letter', + 'first-line', + 'grammar-error', + 'marker', + 'part', + 'placeholder', + 'selection', + 'slotted', + 'spelling-error' + ]; + + const ATTRIBUTES = [ + 'align-content', + 'align-items', + 'align-self', + 'animation', + 'animation-delay', + 'animation-direction', + 'animation-duration', + 'animation-fill-mode', + 'animation-iteration-count', + 'animation-name', + 'animation-play-state', + 'animation-timing-function', + 'auto', + 'backface-visibility', + 'background', + 'background-attachment', + 'background-clip', + 'background-color', + 'background-image', + 'background-origin', + 'background-position', + 'background-repeat', + 'background-size', + 'border', + 'border-bottom', + 'border-bottom-color', + 'border-bottom-left-radius', + 'border-bottom-right-radius', + 'border-bottom-style', + 'border-bottom-width', + 'border-collapse', + 'border-color', + 'border-image', + 'border-image-outset', + 'border-image-repeat', + 'border-image-slice', + 'border-image-source', + 'border-image-width', + 'border-left', + 'border-left-color', + 'border-left-style', + 'border-left-width', + 'border-radius', + 'border-right', + 'border-right-color', + 'border-right-style', + 'border-right-width', + 'border-spacing', + 'border-style', + 'border-top', + 'border-top-color', + 'border-top-left-radius', + 'border-top-right-radius', + 'border-top-style', + 'border-top-width', + 'border-width', + 'bottom', + 'box-decoration-break', + 'box-shadow', + 'box-sizing', + 'break-after', + 'break-before', + 'break-inside', + 'caption-side', + 'clear', + 'clip', + 'clip-path', + 'color', + 'column-count', + 'column-fill', + 'column-gap', + 'column-rule', + 'column-rule-color', + 'column-rule-style', + 'column-rule-width', + 'column-span', + 'column-width', + 'columns', + 'content', + 'counter-increment', + 'counter-reset', + 'cursor', + 'direction', + 'display', + 'empty-cells', + 'filter', + 'flex', + 'flex-basis', + 'flex-direction', + 'flex-flow', + 'flex-grow', + 'flex-shrink', + 'flex-wrap', + 'float', + 'font', + 'font-display', + 'font-family', + 'font-feature-settings', + 'font-kerning', + 'font-language-override', + 'font-size', + 'font-size-adjust', + 'font-smoothing', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-variant-ligatures', + 'font-variation-settings', + 'font-weight', + 'height', + 'hyphens', + 'icon', + 'image-orientation', + 'image-rendering', + 'image-resolution', + 'ime-mode', + 'inherit', + 'initial', + 'justify-content', + 'left', + 'letter-spacing', + 'line-height', + 'list-style', + 'list-style-image', + 'list-style-position', + 'list-style-type', + 'margin', + 'margin-bottom', + 'margin-left', + 'margin-right', + 'margin-top', + 'marks', + 'mask', + 'max-height', + 'max-width', + 'min-height', + 'min-width', + 'nav-down', + 'nav-index', + 'nav-left', + 'nav-right', + 'nav-up', + 'none', + 'normal', + 'object-fit', + 'object-position', + 'opacity', + 'order', + 'orphans', + 'outline', + 'outline-color', + 'outline-offset', + 'outline-style', + 'outline-width', + 'overflow', + 'overflow-wrap', + 'overflow-x', + 'overflow-y', + 'padding', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'page-break-after', + 'page-break-before', + 'page-break-inside', + 'perspective', + 'perspective-origin', + 'pointer-events', + 'position', + 'quotes', + 'resize', + 'right', + 'src', // @font-face + 'tab-size', + 'table-layout', + 'text-align', + 'text-align-last', + 'text-decoration', + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-indent', + 'text-overflow', + 'text-rendering', + 'text-shadow', + 'text-transform', + 'text-underline-position', + 'top', + 'transform', + 'transform-origin', + 'transform-style', + 'transition', + 'transition-delay', + 'transition-duration', + 'transition-property', + 'transition-timing-function', + 'unicode-bidi', + 'vertical-align', + 'visibility', + 'white-space', + 'widows', + 'width', + 'word-break', + 'word-spacing', + 'word-wrap', + 'z-index' + // reverse makes sure longer attributes `font-weight` are matched fully + // instead of getting false positives on say `font` + ].reverse(); + + // some grammars use them all as a single group + const PSEUDO_SELECTORS = PSEUDO_CLASSES.concat(PSEUDO_ELEMENTS); + + + // https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10 + var decimalDigits = '[0-9](_*[0-9])*'; + var frac = `\\.(${decimalDigits})`; + var hexDigits = '[0-9a-fA-F](_*[0-9a-fA-F])*'; + var NUMERIC = { + className: 'number', + variants: [ + // DecimalFloatingPointLiteral + // including ExponentPart + { + begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})[fFdD]?\\b` + }, + // excluding ExponentPart + { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` }, + { begin: `(${frac})[fFdD]?\\b` }, + { begin: `\\b(${decimalDigits})[fFdD]\\b` }, + + // HexadecimalFloatingPointLiteral + { + begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))` + + `[pP][+-]?(${decimalDigits})[fFdD]?\\b` + }, + + // DecimalIntegerLiteral + { begin: '\\b(0|[1-9](_*[0-9])*)[lL]?\\b' }, + + // HexIntegerLiteral + { begin: `\\b0[xX](${hexDigits})[lL]?\\b` }, + + // OctalIntegerLiteral + { begin: '\\b0(_*[0-7])*[lL]?\\b' }, + + // BinaryIntegerLiteral + { begin: '\\b0[bB][01](_*[01])*[lL]?\\b' }, + ], + relevance: 0 + }; + + + /** + * Allows recursive regex expressions to a given depth + * + * ie: recurRegex("(abc~~~)", /~~~/g, 2) becomes: + * (abc(abc(abc))) + * + * @param {string} re + * @param {RegExp} substitution (should be a g mode regex) + * @param {number} depth + * @returns {string}`` + */ + function recurRegex(re, substitution, depth) { + if (depth === -1) return ""; + + return re.replace(substitution, _ => { + return recurRegex(re, substitution, depth - 1); + }); + } + + const IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; + const KEYWORDS = [ + "as", // for exports + "in", + "of", + "if", + "for", + "while", + "finally", + "var", + "new", + "function", + "do", + "return", + "void", + "else", + "break", + "catch", + "instanceof", + "with", + "throw", + "case", + "default", + "try", + "switch", + "continue", + "typeof", + "delete", + "let", + "yield", + "const", + "class", + // JS handles these with a special rule + // "get", + // "set", + "debugger", + "async", + "await", + "static", + "import", + "from", + "export", + "extends" + ]; + const LITERALS = [ + "true", + "false", + "null", + "undefined", + "NaN", + "Infinity" + ]; + + const TYPES = [ + "Intl", + "DataView", + "Number", + "Math", + "Date", + "String", + "RegExp", + "Object", + "Function", + "Boolean", + "Error", + "Symbol", + "Set", + "Map", + "WeakSet", + "WeakMap", + "Proxy", + "Reflect", + "JSON", + "Promise", + "Float64Array", + "Int16Array", + "Int32Array", + "Int8Array", + "Uint16Array", + "Uint32Array", + "Float32Array", + "Array", + "Uint8Array", + "Uint8ClampedArray", + "ArrayBuffer", + "BigInt64Array", + "BigUint64Array", + "BigInt" + ]; + + const ERROR_TYPES = [ + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError" + ]; + + const BUILT_IN_GLOBALS = [ + "setInterval", + "setTimeout", + "clearInterval", + "clearTimeout", + + "require", + "exports", + + "eval", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "unescape" + ]; + + const BUILT_IN_VARIABLES = [ + "arguments", + "this", + "super", + "console", + "window", + "document", + "localStorage", + "module", + "global" // Node.js + ]; + + const BUILT_INS = [].concat( + BUILT_IN_GLOBALS, + TYPES, + ERROR_TYPES + ); + + /* + Language: JavaScript + Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. + Category: common, scripting, web + Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript + */ + + /** @type LanguageFn */ + function javascript(hljs) { + /** + * Takes a string like " { + const tag = "', + end: '' + }; + const XML_TAG = { + begin: /<[A-Za-z0-9\\._:-]+/, + end: /\/[A-Za-z0-9\\._:-]+>|\/>/, + /** + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + isTrulyOpeningTag: (match, response) => { + const afterMatchIndex = match[0].length + match.index; + const nextChar = match.input[afterMatchIndex]; + // nested type? + // HTML should not include another raw `<` inside a tag + // But a type might: `>`, etc. + if (nextChar === "<") { + response.ignoreMatch(); + return; + } + // + // This is now either a tag or a type. + if (nextChar === ">") { + // if we cannot find a matching closing tag, then we + // will ignore it + if (!hasClosingTag(match, { after: afterMatchIndex })) { + response.ignoreMatch(); + } + } + } + }; + const KEYWORDS$1 = { + $pattern: IDENT_RE, + keyword: KEYWORDS, + literal: LITERALS, + built_in: BUILT_INS, + "variable.language": BUILT_IN_VARIABLES + }; + + // https://tc39.es/ecma262/#sec-literals-numeric-literals + const decimalDigits = '[0-9](_?[0-9])*'; + const frac = `\\.(${decimalDigits})`; + // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`; + const NUMBER = { + className: 'number', + variants: [ + // DecimalLiteral + { + begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})\\b` + }, + { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` }, + + // DecimalBigIntegerLiteral + { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` }, + + // NonDecimalIntegerLiteral + { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" }, + { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" }, + { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" }, + + // LegacyOctalIntegerLiteral (does not include underscore separators) + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + { begin: "\\b0[0-7]+n?\\b" }, + ], + relevance: 0 + }; + + const SUBST = { + className: 'subst', + begin: '\\$\\{', + end: '\\}', + keywords: KEYWORDS$1, + contains: [] // defined later + }; + const HTML_TEMPLATE = { + begin: 'html`', + end: '', + starts: { + end: '`', + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ], + subLanguage: 'xml' + } + }; + const CSS_TEMPLATE = { + begin: 'css`', + end: '', + starts: { + end: '`', + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ], + subLanguage: 'css' + } + }; + const TEMPLATE_STRING = { + className: 'string', + begin: '`', + end: '`', + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ] + }; + const JSDOC_COMMENT = hljs.COMMENT( + /\/\*\*(?!\/)/, + '\\*/', + { + relevance: 0, + contains: [ + { + begin: '(?=@[A-Za-z]+)', + relevance: 0, + contains: [ + { + className: 'doctag', + begin: '@[A-Za-z]+' + }, + { + className: 'type', + begin: '\\{', + end: '\\}', + excludeEnd: true, + excludeBegin: true, + relevance: 0 + }, + { + className: 'variable', + begin: IDENT_RE$1 + '(?=\\s*(-)|$)', + endsParent: true, + relevance: 0 + }, + // eat spaces (not newlines) so we can find + // types or variables + { + begin: /(?=[^\n])\s/, + relevance: 0 + } + ] + } + ] + } + ); + const COMMENT = { + className: "comment", + variants: [ + JSDOC_COMMENT, + hljs.C_BLOCK_COMMENT_MODE, + hljs.C_LINE_COMMENT_MODE + ] + }; + const SUBST_INTERNALS = [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + TEMPLATE_STRING, + NUMBER, + hljs.REGEXP_MODE + ]; + SUBST.contains = SUBST_INTERNALS + .concat({ + // we need to pair up {} inside our subst to prevent + // it from ending too early by matching another } + begin: /\{/, + end: /\}/, + keywords: KEYWORDS$1, + contains: [ + "self" + ].concat(SUBST_INTERNALS) + }); + const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains); + const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([ + // eat recursive parens in sub expressions + { + begin: /\(/, + end: /\)/, + keywords: KEYWORDS$1, + contains: ["self"].concat(SUBST_AND_COMMENTS) + } + ]); + const PARAMS = { + className: 'params', + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS + }; + + // ES6 classes + const CLASS_OR_EXTENDS = { + variants: [ + { + match: [ + /class/, + /\s+/, + IDENT_RE$1 + ], + scope: { + 1: "keyword", + 3: "title.class" + } + }, + { + match: [ + /extends/, + /\s+/, + concat(IDENT_RE$1, "(", concat(/\./, IDENT_RE$1), ")*") + ], + scope: { + 1: "keyword", + 3: "title.class.inherited" + } + } + ] + }; + + const CLASS_REFERENCE = { + relevance: 0, + match: /\b[A-Z][a-z]+([A-Z][a-z]+)*/, + className: "title.class", + keywords: { + _: [ + // se we still get relevance credit for JS library classes + ...TYPES, + ...ERROR_TYPES + ] + } + }; + + const USE_STRICT = { + label: "use_strict", + className: 'meta', + relevance: 10, + begin: /^\s*['"]use (strict|asm)['"]/ + }; + + const FUNCTION_DEFINITION = { + variants: [ + { + match: [ + /function/, + /\s+/, + IDENT_RE$1, + /(?=\s*\()/ + ] + }, + // anonymous function + { + match: [ + /function/, + /\s*(?=\()/ + ] + } + ], + className: { + 1: "keyword", + 3: "title.function" + }, + label: "func.def", + contains: [PARAMS], + illegal: /%/ + }; + + const UPPER_CASE_CONSTANT = { + relevance: 0, + match: /\b[A-Z][A-Z_]+\b/, + className: "variable.constant" + }; + + function noneOf(list) { + return concat("(?!", list.join("|"), ")"); + } + + const FUNCTION_CALL = { + match: concat( + /\b/, + noneOf([ + ...BUILT_IN_GLOBALS, + "super" + ]), + IDENT_RE$1, lookahead(/\(/)), + className: "title.function", + relevance: 0 + }; + + const PROPERTY_ACCESS = { + begin: concat(/\./, lookahead( + concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/) + )), + end: IDENT_RE$1, + excludeBegin: true, + keywords: "prototype", + className: "property", + relevance: 0 + }; + + const GETTER_OR_SETTER = { + match: [ + /get|set/, + /\s+/, + IDENT_RE$1, + /(?=\()/ + ], + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [ + { // eat to avoid empty params + begin: /\(\)/ + }, + PARAMS + ] + }; + + const FUNC_LEAD_IN_RE = '(\\(' + + '[^()]*(\\(' + + '[^()]*(\\(' + + '[^()]*' + + '\\)[^()]*)*' + + '\\)[^()]*)*' + + '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>'; + + const FUNCTION_VARIABLE = { + match: [ + /const|var|let/, /\s+/, + IDENT_RE$1, /\s*/, + /=\s*/, + lookahead(FUNC_LEAD_IN_RE) + ], + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [ + PARAMS + ] + }; + + return { + name: 'Javascript', + aliases: ['js', 'jsx', 'mjs', 'cjs'], + keywords: KEYWORDS$1, + // this will be extended by TypeScript + exports: { PARAMS_CONTAINS }, + illegal: /#(?![$_A-z])/, + contains: [ + hljs.SHEBANG({ + label: "shebang", + binary: "node", + relevance: 5 + }), + USE_STRICT, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + TEMPLATE_STRING, + COMMENT, + NUMBER, + CLASS_REFERENCE, + { + className: 'attr', + begin: IDENT_RE$1 + lookahead(':'), + relevance: 0 + }, + FUNCTION_VARIABLE, + { // "value" container + begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', + keywords: 'return throw case', + relevance: 0, + contains: [ + COMMENT, + hljs.REGEXP_MODE, + { + className: 'function', + // we have to count the parens to make sure we actually have the + // correct bounding ( ) before the =>. There could be any number of + // sub-expressions inside also surrounded by parens. + begin: FUNC_LEAD_IN_RE, + returnBegin: true, + end: '\\s*=>', + contains: [ + { + className: 'params', + variants: [ + { + begin: hljs.UNDERSCORE_IDENT_RE, + relevance: 0 + }, + { + className: null, + begin: /\(\s*\)/, + skip: true + }, + { + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS + } + ] + } + ] + }, + { // could be a comma delimited list of params to a function call + begin: /,/, + relevance: 0 + }, + { + match: /\s+/, + relevance: 0 + }, + { // JSX + variants: [ + { begin: FRAGMENT.begin, end: FRAGMENT.end }, + { + begin: XML_TAG.begin, + // we carefully check the opening tag to see if it truly + // is a tag and not a false positive + 'on:begin': XML_TAG.isTrulyOpeningTag, + end: XML_TAG.end + } + ], + subLanguage: 'xml', + contains: [ + { + begin: XML_TAG.begin, + end: XML_TAG.end, + skip: true, + contains: ['self'] + } + ] + } + ], + }, + FUNCTION_DEFINITION, + { + // prevent this from getting swallowed up by function + // since they appear "function like" + beginKeywords: "while if switch catch for" + }, + { + // we have to count the parens to make sure we actually have the correct + // bounding ( ). There could be any number of sub-expressions inside + // also surrounded by parens. + begin: '\\b(?!function)' + hljs.UNDERSCORE_IDENT_RE + + '\\(' + // first parens + '[^()]*(\\(' + + '[^()]*(\\(' + + '[^()]*' + + '\\)[^()]*)*' + + '\\)[^()]*)*' + + '\\)\\s*\\{', // end parens + returnBegin: true, + label: "func.def", + contains: [ + PARAMS, + hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: "title.function" }) + ] + }, + // catch ... so it won't trigger the property rule below + { + match: /\.\.\./, + relevance: 0 + }, + PROPERTY_ACCESS, + // hack: prevents detection of keywords in some circumstances + // .keyword() + // $keyword = x + { + match: '\\$' + IDENT_RE$1, + relevance: 0 + }, + { + match: [/\bconstructor(?=\s*\()/], + className: { 1: "title.function" }, + contains: [PARAMS] + }, + FUNCTION_CALL, + UPPER_CASE_CONSTANT, + CLASS_OR_EXTENDS, + GETTER_OR_SETTER, + { + match: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` + } + ] + }; + } + + /* + Language: JSON + Description: JSON (JavaScript Object Notation) is a lightweight data-interchange format. + Author: Ivan Sagalaev + Website: http://www.json.org + Category: common, protocols, web + */ + + function json(hljs) { + const ATTRIBUTE = { + className: 'attr', + begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/, + relevance: 1.01 + }; + const PUNCTUATION = { + match: /[{}[\],:]/, + className: "punctuation", + relevance: 0 + }; + // normally we would rely on `keywords` for this but using a mode here allows us + // to use the very tight `illegal: \S` rule later to flag any other character + // as illegal indicating that despite looking like JSON we do not truly have + // JSON and thus improve false-positively greatly since JSON will try and claim + // all sorts of JSON looking stuff + const LITERALS = { + beginKeywords: [ + "true", + "false", + "null" + ].join(" ") + }; + + return { + name: 'JSON', + contains: [ + ATTRIBUTE, + PUNCTUATION, + hljs.QUOTE_STRING_MODE, + LITERALS, + hljs.C_NUMBER_MODE, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE + ], + illegal: '\\S' + }; + } + + + /** @type LanguageFn */ + function xml(hljs) { + // Element names can contain letters, digits, hyphens, underscores, and periods + const TAG_NAME_RE = concat(/[A-Z_]/, optional(/[A-Z0-9_.-]*:/), /[A-Z0-9_.-]*/); + const XML_IDENT_RE = /[A-Za-z0-9._:-]+/; + const XML_ENTITIES = { + className: 'symbol', + begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ + }; + const XML_META_KEYWORDS = { + begin: /\s/, + contains: [ + { + className: 'keyword', + begin: /#?[a-z_][a-z1-9_-]+/, + illegal: /\n/ + } + ] + }; + const XML_META_PAR_KEYWORDS = hljs.inherit(XML_META_KEYWORDS, { + begin: /\(/, + end: /\)/ + }); + const APOS_META_STRING_MODE = hljs.inherit(hljs.APOS_STRING_MODE, { + className: 'string' + }); + const QUOTE_META_STRING_MODE = hljs.inherit(hljs.QUOTE_STRING_MODE, { + className: 'string' + }); + const TAG_INTERNALS = { + endsWithParent: true, + illegal: /`]+/ + } + ] + } + ] + } + ] + }; + return { + name: 'HTML, XML', + aliases: [ + 'html', + 'xhtml', + 'rss', + 'atom', + 'xjb', + 'xsd', + 'xsl', + 'plist', + 'wsf', + 'svg' + ], + case_insensitive: true, + contains: [ + { + className: 'meta', + begin: //, + relevance: 10, + contains: [ + XML_META_KEYWORDS, + QUOTE_META_STRING_MODE, + APOS_META_STRING_MODE, + XML_META_PAR_KEYWORDS, + { + begin: /\[/, + end: /\]/, + contains: [ + { + className: 'meta', + begin: //, + contains: [ + XML_META_KEYWORDS, + XML_META_PAR_KEYWORDS, + QUOTE_META_STRING_MODE, + APOS_META_STRING_MODE + ] + } + ] + } + ] + }, + hljs.COMMENT( + //, + { + relevance: 10 + } + ), + { + begin: //, + relevance: 10 + }, + XML_ENTITIES, + { + className: 'meta', + begin: /<\?xml/, + end: /\?>/, + relevance: 10 + }, + { + className: 'tag', + /* + The lookahead pattern (?=...) ensures that 'begin' only matches + ')/, + end: />/, + keywords: { + name: 'style' + }, + contains: [TAG_INTERNALS], + starts: { + end: /<\/style>/, + returnEnd: true, + subLanguage: [ + 'css', + 'xml' + ] + } + }, + { + className: 'tag', + // See the comment in the