Merge remote-tracking branch 'upstream/unstable' into expand-global-var
This commit is contained in:
commit
8939ecc9c5
2
.github/workflows/auto-close-stale-pr.yml
vendored
2
.github/workflows/auto-close-stale-pr.yml
vendored
|
@ -18,4 +18,4 @@ jobs:
|
|||
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 }}
|
||||
|
|
3
.github/workflows/automations.yml
vendored
3
.github/workflows/automations.yml
vendored
|
@ -30,4 +30,5 @@ jobs:
|
|||
uses: eps1lon/actions-label-merge-conflict@releases/2.x
|
||||
with:
|
||||
dirtyLabel: "merge conflict"
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentOnDirty: "This pull request has merge conflicts. Please resolve the conflicts so the PR can be reviewed. Thanks!"
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
2
.github/workflows/build-dev.yml
vendored
2
.github/workflows/build-dev.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
|
6
.github/workflows/build-prod.yml
vendored
6
.github/workflows/build-prod.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master (the latest release)
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
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@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Save new package.json version
|
||||
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: package.json version must be updated
|
||||
|
@ -61,7 +61,7 @@ jobs:
|
|||
prod:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,18 +1,17 @@
|
|||
*.svg
|
||||
jellyfin-roku.zip
|
||||
source/globals.brs
|
||||
.env
|
||||
|
||||
###BrightScript specific
|
||||
# BrightScript
|
||||
dist/apps
|
||||
out/
|
||||
|
||||
build/
|
||||
roku_modules
|
||||
|
||||
#NPM modules
|
||||
source/globals.brs
|
||||
jellyfin-roku.zip
|
||||
# Rooibos
|
||||
bsconfig-tdd.json
|
||||
# NPM
|
||||
node_modules/
|
||||
|
||||
#Eclipse
|
||||
# Eclipse
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
418
.vscode/brighterscript.code-snippets
vendored
Normal file
418
.vscode/brighterscript.code-snippets
vendored
Normal file
|
@ -0,0 +1,418 @@
|
|||
{
|
||||
"rooibos beforeEach": {
|
||||
"prefix": "beforeEach",
|
||||
"body": [
|
||||
"@beforeEach",
|
||||
"function ${2:namespace}_${3:itGroup}_beforeEach()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos afterEach": {
|
||||
"prefix": "afterEach",
|
||||
"body": [
|
||||
"@afterEach",
|
||||
"function ${2:namespace}_${3:itGroup}_afterEach()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos setup": {
|
||||
"prefix": "setup",
|
||||
"body": [
|
||||
"@setup",
|
||||
"function ${2:namespace}_setup()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos tearDown": {
|
||||
"prefix": "tearDown",
|
||||
"body": [
|
||||
"@tearDown",
|
||||
"function ${2:namespace}_tearDown()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos ignore": {
|
||||
"prefix": "ignore",
|
||||
"body": [
|
||||
"@ignore ${1:reason}",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos only": {
|
||||
"prefix": "only",
|
||||
"body": [
|
||||
"@only",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos testSuite": {
|
||||
"prefix": "suite",
|
||||
"body": [
|
||||
"@suite(\"$1\")",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos testcase": {
|
||||
"prefix": "it",
|
||||
"body": [
|
||||
"@it(\"$1\")",
|
||||
"function _()",
|
||||
"\t$0",
|
||||
"end function"
|
||||
]
|
||||
},
|
||||
"rooibos params": {
|
||||
"prefix": "params",
|
||||
"body": [
|
||||
"@params(${1:values})$0"
|
||||
]
|
||||
},
|
||||
"rooibos it": {
|
||||
"prefix": "describe",
|
||||
"body": [
|
||||
"'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++",
|
||||
"@describe(\"${1:groupName}\")",
|
||||
"'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++",
|
||||
"",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos stub": {
|
||||
"prefix": "stub",
|
||||
"body": [
|
||||
"m.stub(${1:target}, \"${2:methodName}\", [${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos mock": {
|
||||
"prefix": "expect",
|
||||
"body": [
|
||||
"${1:mockName} = m.mock(${2:target}, \"${3:methodName}\", ${4:expectedNumberOfcalls}, [${5:methodArgs}], ${6:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expect": {
|
||||
"prefix": "expect",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"${2:methodName}\", ${3:expectedNumberOfcalls}, [${4:methodArgs}], ${5:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectOnce": {
|
||||
"prefix": "expectOnce",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"${2:methodName}\", [${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectCallfunc": {
|
||||
"prefix": "expectCallfunc",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"callFunc\", [\"${2:methodName}\", ${3:methodArgs}], ${4:result})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectObserveNodeField": {
|
||||
"prefix": "eonf",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"observeNodeField\", [${2:node},\"${3:fieldName}\", m.${4:callback}])",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectUnObserveNodeField": {
|
||||
"prefix": "eunf",
|
||||
"body": [
|
||||
"m.expectOnce(${1:target}, \"unobserveNodeField\", [${2:node},\"${:fieldName}\", m.${4:callback}])",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectObjectOnce": {
|
||||
"prefix": "expectObjectOnce",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"${3:methodName}\", [${4:methodArgs}], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectGetInstance": {
|
||||
"prefix": "expectGetInstance",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"getInstance\", [\"${3:instanceName}\"], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectCreateSGNode": {
|
||||
"prefix": "expectCreateSGNode",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"createSGNode\", [\"${3:nodeType}\"$0], ${1:name})"
|
||||
]
|
||||
},
|
||||
"rooibos expectGetClassInstance": {
|
||||
"prefix": "expectGetClassInstance",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"getClassInstance\", [\"${3:instanceName}\"], ${1:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectExpectOnce": {
|
||||
"prefix": "expectExpect",
|
||||
"body": [
|
||||
"${1:name} = { \"id\" : \"${1:name}\" }",
|
||||
"m.expectOnce(${2:target}, \"${3:methodName}\", [${4:methodArgs}], ${1:name})",
|
||||
"m.expectOnce(${1:name}, \"${5:methodName}\", [${6:methodArgs}], ${7:name})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos expectNone": {
|
||||
"prefix": "expectNone",
|
||||
"body": [
|
||||
"m.expectNone(${1:target}, \"${2:methodName}\")",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertFalse": {
|
||||
"prefix": "assertFalse",
|
||||
"body": [
|
||||
"m.assertFalse(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAsync": {
|
||||
"prefix": "assertAsync",
|
||||
"body": [
|
||||
"m.AssertAsyncField(${1:value}, $2{:fieldName})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertTrue": {
|
||||
"prefix": "assertTrue",
|
||||
"body": [
|
||||
"m.assertTrue(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertEqual": {
|
||||
"prefix": "assertEqual",
|
||||
"body": [
|
||||
"m.assertEqual(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertLike": {
|
||||
"prefix": "assertLike",
|
||||
"body": [
|
||||
"m.assertLike(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotEqual": {
|
||||
"prefix": "assertNotEqual",
|
||||
"body": [
|
||||
"m.assertNotEqual(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertInvalid": {
|
||||
"prefix": "assertInvalid",
|
||||
"body": [
|
||||
"m.assertInvalid(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotInvalid": {
|
||||
"prefix": "assertNotInvalid",
|
||||
"body": [
|
||||
"m.assertNotInvalid(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAHasKey": {
|
||||
"prefix": "assertAAHasKey",
|
||||
"body": [
|
||||
"m.assertAAHasKey(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAANotHasKey": {
|
||||
"prefix": "assertAANotHasKey",
|
||||
"body": [
|
||||
"m.assertAANotHasKey(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAHasKeys": {
|
||||
"prefix": "assertAAHasKeys",
|
||||
"body": [
|
||||
"m.assertAAHasKeys(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAANotHasKeys": {
|
||||
"prefix": "assertAANotHasKeys",
|
||||
"body": [
|
||||
"m.assertAANotHasKeys(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContains": {
|
||||
"prefix": "assertArrayContains",
|
||||
"body": [
|
||||
"m.assertArrayContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotContains": {
|
||||
"prefix": "assertArrayNotContains",
|
||||
"body": [
|
||||
"m.assertArrayNotContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsSubset": {
|
||||
"prefix": "assertArrayContainsSubset",
|
||||
"body": [
|
||||
"m.assertArrayContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsAAs": {
|
||||
"prefix": "assertArrayContainsAAs",
|
||||
"body": [
|
||||
"m.assertArrayContainsAAs(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotContainsSubset": {
|
||||
"prefix": "assertArrayNotContainsSubset",
|
||||
"body": [
|
||||
"m.assertArrayNotContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayCount": {
|
||||
"prefix": "assertArrayCount",
|
||||
"body": [
|
||||
"m.assertArrayCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayNotCount": {
|
||||
"prefix": "assertArrayNotCount",
|
||||
"body": [
|
||||
"m.assertArrayNotCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertEmpty": {
|
||||
"prefix": "assertEmpty",
|
||||
"body": [
|
||||
"m.assertEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNotEmpty": {
|
||||
"prefix": "assertNotEmpty",
|
||||
"body": [
|
||||
"m.assertNotEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertArrayContainsOnlyValuesOfType": {
|
||||
"prefix": "assertArrayContainsOnlyValuesOfType",
|
||||
"body": [
|
||||
"m.assertArrayContainsOnlyValuesOfType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertType": {
|
||||
"prefix": "assertType",
|
||||
"body": [
|
||||
"m.assertType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertSubType": {
|
||||
"prefix": "assertSubType",
|
||||
"body": [
|
||||
"m.assertSubType(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeCount": {
|
||||
"prefix": "assertNodeCount",
|
||||
"body": [
|
||||
"m.assertNodeCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotCount": {
|
||||
"prefix": "assertNodeNotCount",
|
||||
"body": [
|
||||
"m.assertNodeNotCount(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeEmpty": {
|
||||
"prefix": "assertNodeEmpty",
|
||||
"body": [
|
||||
"m.assertNodeEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotEmpty": {
|
||||
"prefix": "assertNodeNotEmpty",
|
||||
"body": [
|
||||
"m.assertNodeNotEmpty(${1:value})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeContains": {
|
||||
"prefix": "assertNodeContains",
|
||||
"body": [
|
||||
"m.assertNodeContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotContains": {
|
||||
"prefix": "assertNodeNotContains",
|
||||
"body": [
|
||||
"m.assertNodeNotContains(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeContainsFields": {
|
||||
"prefix": "assertNodeContainsFields",
|
||||
"body": [
|
||||
"m.assertNodeContainsFields(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertNodeNotContainsFields": {
|
||||
"prefix": "assertNodeNotContainsFields",
|
||||
"body": [
|
||||
"m.assertNodeNotContainsFields(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertAAContainsSubset": {
|
||||
"prefix": "assertAAContainsSubset",
|
||||
"body": [
|
||||
"m.assertAAContainsSubset(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"rooibos assertMocks": {
|
||||
"prefix": "assertMocks",
|
||||
"body": [
|
||||
"m.assertMocks(${1:value}, ${2:expected})",
|
||||
"$0"
|
||||
]
|
||||
}
|
||||
}
|
66
.vscode/launch.json
vendored
66
.vscode/launch.json
vendored
|
@ -4,8 +4,12 @@
|
|||
{
|
||||
"type": "brightscript",
|
||||
"request": "launch",
|
||||
"name": "Jellyfin Debug: Launch",
|
||||
"name": "Jellyfin Debug",
|
||||
"stopOnEntry": false,
|
||||
// To enable RALE:
|
||||
// set "brightscript.debug.raleTrackerTaskFileLocation": "/absolute/path/to/rale/TrackerTask.xml" in your vscode user settings
|
||||
// set the below field to true
|
||||
"injectRaleTrackerTask": false,
|
||||
//WARNING: don't edit this value. Instead, set "brightscript.debug.host": "YOUR_HOST_HERE" in your vscode user settings
|
||||
//"host": "${promptForHost}",
|
||||
//WARNING: don't edit this value. Instead, set "brightscript.debug.password": "YOUR_PASSWORD_HERE" in your vscode user settings
|
||||
|
@ -18,6 +22,66 @@
|
|||
"source/**/*",
|
||||
"manifest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Run tests",
|
||||
"type": "brightscript",
|
||||
"request": "launch",
|
||||
"consoleOutput": "full",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"preLaunchTask": "build-tests",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
@ -2,6 +2,13 @@
|
|||
"files.associations": {
|
||||
"*.ts": "xml"
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-xml"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
},
|
||||
"xml.format.maxLineWidth": 0,
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"brightscript.bsdk": "node_modules/brighterscript"
|
||||
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build-tests",
|
||||
"type": "shell",
|
||||
"command": "npm run build-tests",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"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": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -27,7 +27,7 @@ Follow the steps below to install the app on your personal Roku device. This wil
|
|||
|
||||
## Developer Mode
|
||||
|
||||
Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created, you will need these later.
|
||||
Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created - you will need these!
|
||||
|
||||
## Clone the GitHub Repo
|
||||
|
||||
|
@ -71,7 +71,7 @@ That's it! VSCode will auto-package the project, sideload it to the specified de
|
|||
|
||||
Out of the box, the BrightScript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your VSCode user settings:
|
||||
|
||||
```js
|
||||
```json
|
||||
{
|
||||
"brightscript.debug.host": "YOUR_ROKU_HOST_HERE",
|
||||
"brightscript.debug.password": "YOUR_ROKU_DEV_PASSWORD_HERE",
|
||||
|
|
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@
|
|||
##########################################################################
|
||||
|
||||
APPNAME = Jellyfin_Roku
|
||||
VERSION = 1.6.4
|
||||
VERSION = 1.6.5
|
||||
|
||||
ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
|
||||
|
||||
|
|
62
README.md
62
README.md
|
@ -1,39 +1,41 @@
|
|||
<h1 style="text-align: center;">Jellyfin app for Roku</h1>
|
||||
<h3 style="text-align: center;">Part of the <a href="https://jellyfin.media/">Jellyfin</a> Project</h3>
|
||||
<h1 align="center">Jellyfin Roku</h1>
|
||||
<h2 align="center">Part of the <a href="https://jellyfin.org">Jellyfin Project</a></h2>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Logo banner" src="https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true"/>
|
||||
<br/><br/>
|
||||
<a href="https://github.com/jellyfin/jellyfin-roku">
|
||||
<img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-roku.svg"/>
|
||||
</a>
|
||||
<a href="https://github.com/jellyfin/jellyfin-roku/releases">
|
||||
<img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg"/>
|
||||
</a>
|
||||
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget">
|
||||
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg" alt="Translation status" />
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://matrix.to/#/#jellyfin-dev-roku:matrix.org">
|
||||
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/jellyfin">
|
||||
<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
[![Logo Banner](https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true "Jellyfin")](https://jellyfin.org)
|
||||
|
||||
The Jellyfin Roku App is a Jellyfin client for Roku Devices. This is still very much a work in progress, so we would encourage you to [get involved](#get_involved) if you can.
|
||||
[![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)
|
||||
[![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)
|
||||
|
||||
## Getting Started
|
||||
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.
|
||||
|
||||
The channel is available on the [Roku Channel Store](https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin).
|
||||
## Install
|
||||
|
||||
## Getting Involved<a name="get_involved"></a>
|
||||
Download the latest release on the [Roku Channel Store](https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin).
|
||||
|
||||
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a GitHub issue.
|
||||
## Get Involved
|
||||
|
||||
Feature requests are always welcome too, but please have a read though the existing issues to see if someone has already raised one for something similar.
|
||||
No matter what your interests or skills are you can help make this client better for everyone by simply using the client and giving feedback to the developers when things break. [Create an issue](https://github.com/jellyfin/jellyfin-roku/issues/new/choose) here on GitHub or give us a shout on [Matrix](https://matrix.to/#/#jellyfin-dev-roku:matrix.org).
|
||||
|
||||
If you fancy some development, then read the [DEVGUIDE](DEVGUIDE.md) to find out the best ways to help.
|
||||
## Beta Test
|
||||
|
||||
As Roku have severely limited their Beta channel program, the best way to test pre-release versions is by following the [DEVGUIDE](DEVGUIDE.md) to install and test the latest changes. Feedback is always welcome.
|
||||
To test the latest features before they get released:
|
||||
|
||||
1. Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created - you will need these!
|
||||
2. Download the [latest build](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable). Select the first item listed then click the link at the bottom of the page i.e. `Jellyfin-Roku-dev-d3352495c579f6adeca085cdbc137ac36e70d558`. This will download a zip file to your computer.
|
||||
3. Put your Roku's IP from step 1 into a browser i.e. `http://192.168.1.2` and press enter.
|
||||
4. Log in with credentials from step 1.
|
||||
5. Upload and install the zip file downloaded in step 2.
|
||||
|
||||
> NOTE: The beta app will always be at the bottom of your Roku's channel list and it will *not* automatically update.
|
||||
|
||||
## 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).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
New feature requests are always welcome but before creating an issue please read through the [existing issues](https://github.com/jellyfin/jellyfin-roku/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) to see if someone has already raised one for what you're looking for.
|
||||
|
|
44
bsconfig-tests.json
Normal file
44
bsconfig-tests.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"files": [
|
||||
{
|
||||
"src": "test-app/**/*"
|
||||
},
|
||||
{
|
||||
"src": "source/**/!(Main.brs)",
|
||||
"dest": "source"
|
||||
},
|
||||
{
|
||||
"src": "components/**/*",
|
||||
"dest": "components"
|
||||
},
|
||||
{
|
||||
"src": "locale/**/*",
|
||||
"dest": "locale"
|
||||
},
|
||||
{
|
||||
"src": "settings/**/*",
|
||||
"dest": "settings"
|
||||
}
|
||||
],
|
||||
"autoImportComponentScript": true,
|
||||
"createPackage": false,
|
||||
"stagingFolderPath": "build",
|
||||
"plugins": [
|
||||
"rooibos-roku"
|
||||
],
|
||||
"rooibos": {
|
||||
"isRecordingCodeCoverage": false,
|
||||
"testsFilePattern": null,
|
||||
"tags": [
|
||||
"!integration",
|
||||
"!deprecated",
|
||||
"!fixme"
|
||||
],
|
||||
"showOnlyFailures": true,
|
||||
"catchCrashes": true,
|
||||
"lineWidth": 70,
|
||||
"failFast": false,
|
||||
"sendHomeOnFinish": false
|
||||
},
|
||||
"sourceMap": true
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
"settings/*.*"
|
||||
],
|
||||
"plugins": [
|
||||
"@rokucommunity/bslint"
|
||||
"@rokucommunity/bslint",
|
||||
"rooibos-roku"
|
||||
],
|
||||
"diagnosticFilters": [
|
||||
"**/roku_modules/**/*",
|
||||
"**/testFramework/*",
|
||||
"**/tests/*"
|
||||
"node_modules/**",
|
||||
"**/roku_modules/**"
|
||||
]
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"files": [
|
||||
"source/**/*.brs",
|
||||
"components/**/*.brs"
|
||||
"source/**/*.bs",
|
||||
"components/**/*.brs",
|
||||
"components/**/*.bs",
|
||||
"test-app/**/*.bs"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -44,7 +44,6 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
|||
videotype = LCase(meta.type)
|
||||
|
||||
if videotype = "episode" or videotype = "series"
|
||||
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
|
||||
video.content.contenttype = "episode"
|
||||
end if
|
||||
|
||||
|
|
|
@ -23,8 +23,10 @@ sub init()
|
|||
m.overlayMinutes = m.top.findNode("overlayMinutes")
|
||||
m.overlayMeridian = m.top.findNode("overlayMeridian")
|
||||
m.overlayMeridian.font.size = 20
|
||||
' start timer
|
||||
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
|
||||
|
|
|
@ -20,6 +20,11 @@ sub init()
|
|||
m.nextEpisodeButton.text = tr("Next Episode")
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
m.nextupbuttonseconds = get_user_setting("playback.nextupbuttonseconds", "30")
|
||||
if isValid(m.nextupbuttonseconds)
|
||||
m.nextupbuttonseconds = val(m.nextupbuttonseconds)
|
||||
else
|
||||
m.nextupbuttonseconds = 30
|
||||
end if
|
||||
|
||||
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
|
||||
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
|
||||
|
@ -28,10 +33,12 @@ sub init()
|
|||
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
|
||||
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
|
||||
|
||||
m.top.observeField("state", "onState")
|
||||
m.top.observeField("content", "onContentChange")
|
||||
m.top.observeField("allowCaptions", "onAllowCaptionsChange")
|
||||
end sub
|
||||
|
||||
sub onAllowCaptionsChange()
|
||||
if not m.top.allowCaptions then return
|
||||
|
||||
'Captions
|
||||
m.captionGroup = m.top.findNode("captionGroup")
|
||||
m.captionGroup.createchildren(9, "LayoutGroup")
|
||||
m.captionTask = createObject("roSGNode", "captionTask")
|
||||
|
@ -85,7 +92,6 @@ end sub
|
|||
'
|
||||
' Runs Next Episode button animation and sets focus to button
|
||||
sub showNextEpisodeButton()
|
||||
if m.top.content.contenttype <> 4 then return
|
||||
if m.global.userConfig.EnableNextEpisodeAutoPlay and not m.nextEpisodeButton.visible
|
||||
m.showNextEpisodeButtonAnimation.control = "start"
|
||||
m.nextEpisodeButton.setFocus(true)
|
||||
|
@ -96,7 +102,7 @@ end sub
|
|||
'
|
||||
'Update count down text
|
||||
sub updateCount()
|
||||
nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
|
||||
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
|
||||
if nextEpisodeCountdown < 0
|
||||
nextEpisodeCountdown = 0
|
||||
end if
|
||||
|
@ -114,8 +120,9 @@ end sub
|
|||
' Checks if we need to display the Next Episode button
|
||||
sub checkTimeToDisplayNextEpisode()
|
||||
if m.top.content.contenttype <> 4 then return
|
||||
if m.nextupbuttonseconds = 0 then return
|
||||
|
||||
if int(m.top.position) >= (m.top.runTime - 30)
|
||||
if int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds)
|
||||
showNextEpisodeButton()
|
||||
updateCount()
|
||||
return
|
||||
|
@ -129,7 +136,9 @@ end sub
|
|||
|
||||
' When Video Player state changes
|
||||
sub onPositionChanged()
|
||||
m.captionTask.currentPos = Int(m.top.position * 1000)
|
||||
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)
|
||||
|
@ -140,7 +149,9 @@ end sub
|
|||
'
|
||||
' When Video Player state changes
|
||||
sub onState(msg)
|
||||
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
|
||||
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
|
||||
|
||||
|
|
|
@ -16,13 +16,12 @@
|
|||
<field id="transcodeAvailable" type="boolean" value="false" />
|
||||
<field id="retryWithTranscoding" type="boolean" value="false" />
|
||||
<field id="isTranscoded" type="boolean" />
|
||||
<field id="allowCaptions" type="boolean" value="false" />
|
||||
<field id="transcodeReasons" type="array" />
|
||||
|
||||
<field id="videoId" type="string" />
|
||||
<field id="mediaSourceId" type="string" />
|
||||
<field id="audioIndex" type="integer" />
|
||||
<field id="runTime" type="integer" />
|
||||
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="JFVideo.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
|
|
|
@ -28,13 +28,12 @@ end sub
|
|||
|
||||
sub setFont()
|
||||
fs = CreateObject("roFileSystem")
|
||||
fontlist = fs.Find("tmp:/", "font")
|
||||
if fontlist.count() > 0
|
||||
m.font.uri = "tmp:/" + fontlist[0]
|
||||
|
||||
if fs.Exists("tmp:/font")
|
||||
m.font.uri = "tmp:/font"
|
||||
m.font.size = m.fontSize
|
||||
else
|
||||
reg = CreateObject("roFontRegistry")
|
||||
m.font = reg.GetDefaultFont(m.fontSize, false, false)
|
||||
m.font = "font:LargeSystemFont"
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -56,6 +55,7 @@ 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
|
||||
|
@ -89,7 +89,7 @@ function newRect(lg)
|
|||
end function
|
||||
|
||||
|
||||
sub updateCaption ()
|
||||
sub updateCaption()
|
||||
m.top.currentCaption = []
|
||||
if LCase(m.top.playerState) = "playingon"
|
||||
m.top.currentPos = m.top.currentPos + 100
|
||||
|
|
|
@ -166,6 +166,20 @@ sub itemContentChanged()
|
|||
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
|
||||
|
|
|
@ -83,18 +83,6 @@ sub onLibrariesLoaded()
|
|||
|
||||
haveLiveTV = false
|
||||
|
||||
' Load the NextUp Data
|
||||
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
||||
m.LoadNextUpTask.control = "RUN"
|
||||
|
||||
' Load the Continue Watching Data
|
||||
m.LoadContinueTask.observeField("content", "updateContinueItems")
|
||||
m.LoadContinueTask.control = "RUN"
|
||||
|
||||
' Load the Favorites Data
|
||||
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
||||
m.LoadFavoritesTask.control = "RUN"
|
||||
|
||||
' validate library data
|
||||
if isValid(m.libraryData) and m.libraryData.count() > 0
|
||||
userConfig = m.global.userConfig
|
||||
|
@ -112,42 +100,42 @@ sub onLibrariesLoaded()
|
|||
latestInRow = content.CreateChild("HomeRow")
|
||||
latestInRow.title = tr("Latest in") + " " + lib.name + " >"
|
||||
sizeArray.Push([464, 331])
|
||||
|
||||
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"
|
||||
else if lib.collectionType = "livetv"
|
||||
' If we have Live TV, add "On Now"
|
||||
onNowRow = content.CreateChild("HomeRow")
|
||||
onNowRow.title = tr("On Now")
|
||||
sizeArray.Push([464, 331])
|
||||
haveLiveTV = true
|
||||
' If we have Live TV access, load "On Now" data
|
||||
if haveLiveTV
|
||||
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
||||
m.LoadOnNowTask.control = "RUN"
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
m.top.rowItemSize = sizeArray
|
||||
m.top.content = content
|
||||
|
||||
' Load the Continue Watching Data
|
||||
m.LoadContinueTask.observeField("content", "updateContinueItems")
|
||||
m.LoadContinueTask.control = "RUN"
|
||||
|
||||
' Load the Favorites Data
|
||||
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
||||
m.LoadFavoritesTask.control = "RUN"
|
||||
|
||||
' If we have Live TV access, load "On Now" data
|
||||
if haveLiveTV
|
||||
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
||||
m.LoadOnNowTask.control = "RUN"
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub updateHomeRows()
|
||||
if m.global.playstateTask.state = "run"
|
||||
m.global.playstateTask.observeField("state", "updateHomeRows")
|
||||
else
|
||||
m.global.playstateTask.unobserveField("state")
|
||||
return
|
||||
end if
|
||||
|
||||
m.global.playstateTask.unobserveField("state")
|
||||
|
||||
m.LoadContinueTask.observeField("content", "updateContinueItems")
|
||||
m.LoadContinueTask.control = "RUN"
|
||||
end sub
|
||||
|
@ -237,6 +225,9 @@ sub updateContinueItems()
|
|||
homeRows.replaceChild(row, continueRowIndex)
|
||||
end if
|
||||
end if
|
||||
|
||||
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
||||
m.LoadNextUpTask.control = "RUN"
|
||||
end sub
|
||||
|
||||
sub updateNextUpItems()
|
||||
|
@ -289,6 +280,24 @@ sub updateNextUpItems()
|
|||
m.global.app_loaded = true
|
||||
end if
|
||||
|
||||
' create task nodes for "Latest In" rows
|
||||
userConfig = m.global.userConfig
|
||||
filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes)
|
||||
for each lib in filteredLatest
|
||||
if lib.collectionType <> "livetv" and lib.collectionType <> "boxsets" 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
|
||||
|
||||
sub updateLatestItems(msg)
|
||||
|
|
|
@ -132,6 +132,12 @@ sub loadItems()
|
|||
' Skip Books for now as we don't support it (issue #558)
|
||||
if item.Type <> "Book"
|
||||
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
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
sub init()
|
||||
m.queue = []
|
||||
m.originalQueue = []
|
||||
m.queueTypes = []
|
||||
m.position = 0
|
||||
m.shuffleEnabled = false
|
||||
end sub
|
||||
|
||||
' Clear all content from play queue
|
||||
|
@ -27,6 +29,11 @@ function getCurrentItem()
|
|||
return getItemByIndex(m.position)
|
||||
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]
|
||||
|
@ -108,6 +115,54 @@ 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")
|
||||
songIDArray = getQueue()
|
||||
|
||||
' Move the currently playing song to the front of the queue
|
||||
temp = top()
|
||||
songIDArray[0] = getCurrentItem()
|
||||
songIDArray[getPosition()] = temp
|
||||
|
||||
for i = 1 to songIDArray.count() - 1
|
||||
j = Rnd(songIDArray.count() - 1)
|
||||
temp = songIDArray[i]
|
||||
songIDArray[i] = songIDArray[j]
|
||||
songIDArray[j] = temp
|
||||
end for
|
||||
|
||||
set(songIDArray)
|
||||
end sub
|
||||
|
||||
' Return the fitst item in the play queue
|
||||
function top()
|
||||
return getItemByIndex(0)
|
||||
|
@ -115,7 +170,7 @@ end function
|
|||
|
||||
' Replace play queue with passed array
|
||||
sub set(items)
|
||||
setPosition(0)
|
||||
clear()
|
||||
m.queue = items
|
||||
for each item in items
|
||||
m.queueTypes.push(getItemType(item))
|
||||
|
|
|
@ -5,19 +5,23 @@
|
|||
<function name="deleteAtIndex" />
|
||||
<function name="getCount" />
|
||||
<function name="getCurrentItem" />
|
||||
<function name="getIsShuffled" />
|
||||
<function name="getItemByIndex" />
|
||||
<function name="getPosition" />
|
||||
<function name="getQueue" />
|
||||
<function name="getQueueTypes" />
|
||||
<function name="getQueueUniqueTypes" />
|
||||
<function name="getUnshuffledQueue" />
|
||||
<function name="moveBack" />
|
||||
<function name="moveForward" />
|
||||
<function name="peek" />
|
||||
<function name="playQueue" />
|
||||
<function name="pop" />
|
||||
<function name="push" />
|
||||
<function name="resetShuffle" />
|
||||
<function name="set" />
|
||||
<function name="setPosition" />
|
||||
<function name="toggleShuffle" />
|
||||
<function name="top" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="QueueManager.brs" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
sub init()
|
||||
m.playReported = false
|
||||
m.top.disableScreenSaver = true
|
||||
m.top.observeField("state", "audioStateChanged")
|
||||
end sub
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="AudioPlayer" extends="Audio">
|
||||
<component name="AudioPlayer" extends="Video">
|
||||
<interface>
|
||||
<field id="loopMode" type="string" value="" />
|
||||
</interface>
|
||||
|
|
|
@ -10,7 +10,6 @@ sub init()
|
|||
|
||||
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
|
||||
|
||||
m.shuffleEnabled = false
|
||||
m.buttonCount = m.buttons.getChildCount()
|
||||
|
||||
m.screenSaverTimeout = 300
|
||||
|
@ -26,6 +25,8 @@ sub init()
|
|||
|
||||
loadButtons()
|
||||
pageContentChanged()
|
||||
setShuffleIconState()
|
||||
setLoopButtonImage()
|
||||
end sub
|
||||
|
||||
sub onScreensaverTimeoutLoaded()
|
||||
|
@ -111,9 +112,10 @@ sub setupInfoNodes()
|
|||
m.playPosition = m.top.findNode("playPosition")
|
||||
m.bufferPosition = m.top.findNode("bufferPosition")
|
||||
m.seekBar = m.top.findNode("seekBar")
|
||||
m.numberofsongsField = m.top.findNode("numberofsongs")
|
||||
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()
|
||||
|
@ -156,6 +158,13 @@ sub audioPositionChanged()
|
|||
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)
|
||||
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
|
||||
|
@ -254,6 +263,11 @@ function previousClicked() as boolean
|
|||
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()
|
||||
|
||||
|
@ -261,6 +275,11 @@ function previousClicked() as boolean
|
|||
return true
|
||||
end function
|
||||
|
||||
sub resetLoopModeToDefault()
|
||||
m.global.audioPlayer.loopMode = ""
|
||||
setLoopButtonImage()
|
||||
end sub
|
||||
|
||||
function loopClicked() as boolean
|
||||
|
||||
if m.global.audioPlayer.loopMode = ""
|
||||
|
@ -290,6 +309,11 @@ 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
|
||||
|
@ -298,10 +322,12 @@ function nextClicked() as boolean
|
|||
end function
|
||||
|
||||
sub toggleShuffleEnabled()
|
||||
m.shuffleEnabled = not m.shuffleEnabled
|
||||
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
|
||||
|
@ -313,44 +339,36 @@ end function
|
|||
|
||||
function shuffleClicked() as boolean
|
||||
|
||||
currentSongIndex = findCurrentSongIndex(m.global.queueManager.callFunc("getUnshuffledQueue"))
|
||||
|
||||
toggleShuffleEnabled()
|
||||
|
||||
if not m.shuffleEnabled
|
||||
if not m.global.queueManager.callFunc("getIsShuffled")
|
||||
m.shuffleIndicator.opacity = ".4"
|
||||
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-on", "-off")
|
||||
|
||||
currentSongIndex = findCurrentSongIndex(m.originalSongList)
|
||||
m.global.queueManager.callFunc("set", m.originalSongList)
|
||||
m.global.queueManager.callFunc("setPosition", currentSongIndex)
|
||||
setFieldTextValue("numberofsongs", "Track " + stri(m.global.queueManager.callFunc("getPosition") + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
|
||||
|
||||
setTrackNumberDisplay()
|
||||
return true
|
||||
end if
|
||||
|
||||
m.shuffleIndicator.opacity = "1"
|
||||
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
|
||||
|
||||
m.originalSongList = m.global.queueManager.callFunc("getQueue")
|
||||
|
||||
songIDArray = m.global.queueManager.callFunc("getQueue")
|
||||
|
||||
' Move the currently playing song to the front of the queue
|
||||
temp = m.global.queueManager.callFunc("top")
|
||||
songIDArray[0] = m.global.queueManager.callFunc("getCurrentItem")
|
||||
songIDArray[m.global.queueManager.callFunc("getPosition")] = temp
|
||||
|
||||
for i = 1 to songIDArray.count() - 1
|
||||
j = Rnd(songIDArray.count() - 1)
|
||||
temp = songIDArray[i]
|
||||
songIDArray[i] = songIDArray[j]
|
||||
songIDArray[j] = temp
|
||||
end for
|
||||
|
||||
m.global.queueManager.callFunc("set", songIDArray)
|
||||
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"
|
||||
|
@ -400,6 +418,9 @@ sub pageContentChanged()
|
|||
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
|
||||
|
@ -455,6 +476,9 @@ sub onMetaDataLoaded()
|
|||
|
||||
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
|
||||
|
@ -492,14 +516,8 @@ end sub
|
|||
' Populate on screen text variables
|
||||
sub setOnScreenTextValues(json)
|
||||
if isValid(json)
|
||||
currentSongIndex = m.global.queueManager.callFunc("getPosition")
|
||||
|
||||
if m.shuffleEnabled
|
||||
currentSongIndex = findCurrentSongIndex(m.originalSongList)
|
||||
end if
|
||||
|
||||
if m.playlistTypeCount = 1
|
||||
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
|
||||
setTrackNumberDisplay()
|
||||
end if
|
||||
|
||||
setFieldTextValue("artist", json.Artists[0])
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
<Poster id="backdrop" opacity=".5" loadDisplayMode="scaleToZoom" width="1920" height="1200" blendColor="#3f3f3f" />
|
||||
<Poster id="shuffleIndicator" width="64" height="64" uri="pkg:/images/icons/shuffleIndicator-off.png" translation="[1150,775]" opacity="0" />
|
||||
<Poster id="loopIndicator" width="64" height="64" uri="pkg:/images/icons/loopIndicator-off.png" translation="[700,775]" opacity="0" />
|
||||
<Label id="positionTimestamp" width="100" height="25" horizAlign="right" font="font:SmallestSystemFont" translation="[590,825]" color="#999999" text="0:00" />
|
||||
<Label id="totalLengthTimestamp" width="100" height="25" horizAlign="left" font="font:SmallestSystemFont" translation="[1230,825]" color="#999999" />
|
||||
<LayoutGroup id="toplevel" layoutDirection="vert" horizAlignment="center" translation="[960,175]" itemSpacings="[40]">
|
||||
<LayoutGroup id="main_group" layoutDirection="vert" horizAlignment="center" itemSpacings="[15]">
|
||||
<Poster id="albumCover" width="500" height="500" />
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
sub init()
|
||||
' backgroundUri must be set to an empty string before backgroundColor can be set
|
||||
m.top.backgroundUri = ""
|
||||
m.top.backgroundColor = "#000000"
|
||||
|
||||
m.PosterOne = m.top.findNode("PosterOne")
|
||||
m.PosterOne.uri = "pkg:/images/logo.png"
|
||||
|
||||
m.BounceAnimation = m.top.findNode("BounceAnimation")
|
||||
m.BounceAnimation.control = "start" 'Start BounceAnimation
|
||||
|
||||
if get_user_setting("ui.screensaver.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
|
|
@ -1,146 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component name="Screensaver" extends="Scene">
|
||||
|
||||
<script type="text/brightscript" uri = "Screensaver.brs"/>
|
||||
|
||||
<children>
|
||||
<Poster id="backdrop" loadDisplayMode="scaleToZoom" width="1920" height="1200" />
|
||||
|
||||
<!-- Makes 2 Posters on top of each other -->
|
||||
<Poster
|
||||
id = "PosterOne"
|
||||
width = "389"
|
||||
height = "104"
|
||||
opacity = "1"
|
||||
translation = "[960,540]"
|
||||
/>
|
||||
|
||||
<!--Gigantic loop for bouncing animation -->
|
||||
|
||||
<SequentialAnimation
|
||||
id = "BounceAnimation"
|
||||
repeat = "true"
|
||||
>
|
||||
<Animation
|
||||
id = "AnimOne"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "7.2"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[960,540],[1500,250]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimTwo"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "4.5"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[1500,250],[1350,60]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimThree"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "16"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[1350,60],[200,890]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimFour"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "3.2"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[200,890],[30,750]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimFive"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "13.3"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[30,750],[1200,60]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimSix"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "6.7"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[1200,60],[1500,300]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimSeven"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "15"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[1500,300],[150,935]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimEight"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "2"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[150,935],[10,899.3]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
<Animation
|
||||
id = "AnimNine"
|
||||
repeat = "false"
|
||||
easeFunction = "linear"
|
||||
duration = "11"
|
||||
>
|
||||
<Vector2DFieldInterpolator
|
||||
id = "OneInterp"
|
||||
key = "[0.0,1.0]"
|
||||
keyValue = "[[10,899.3],[960,540]]"
|
||||
fieldToInterp = "PosterOne.translation"
|
||||
/>
|
||||
</Animation>
|
||||
</SequentialAnimation>
|
||||
|
||||
</children>
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
|
@ -23,7 +23,6 @@
|
|||
<field id="videoId" type="string" />
|
||||
<field id="mediaSourceId" type="string" />
|
||||
<field id="audioIndex" type="integer" />
|
||||
<field id="runTime" type="integer" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="VideoPlayerView.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
|
|
|
@ -3,6 +3,7 @@ VSCode
|
|||
BrightScript
|
||||
sideload
|
||||
Sideload
|
||||
Reddit
|
||||
DEVGUIDE
|
||||
ing
|
||||
hardcode
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -675,31 +675,13 @@
|
|||
<extracomment>Settings Menu - Title for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</source>
|
||||
<translation>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</translation>
|
||||
<source>Go directly to the episode list if a TV series has only one season.</source>
|
||||
<translation>Go directly to the episode list if a TV series has only one season.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>If enabled, images of unwatched episodes will be blurred.</source>
|
||||
<translation>If enabled, images of unwatched episodes will be blurred.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Screensaver</source>
|
||||
<translation>Screensaver</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Options for Jellyfin's screensaver.</source>
|
||||
<translation>Options for Jellyfin's screensaver.</translation>
|
||||
<extracomment>Description for Screensaver user settings.</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Use Splashscreen as Screensaver</source>
|
||||
<translation>Use Splashscreen as Screensaver</translation>
|
||||
<extracomment>Option Title in user setting screen</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</source>
|
||||
<translation>Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</translation>
|
||||
<source>Blur images of unwatched episodes.</source>
|
||||
<translation>Blur images of unwatched episodes.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Design Elements</source>
|
||||
|
@ -726,8 +708,8 @@
|
|||
<extracomment>Settings Menu - Title for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
|
||||
<translation>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</translation>
|
||||
<source>Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
|
||||
<translation>Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
|
@ -736,8 +718,8 @@
|
|||
<extracomment>Option Title in user setting screen</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</source>
|
||||
<translation>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation>
|
||||
<source>Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.</source>
|
||||
<translation>Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
|
@ -1025,36 +1007,36 @@
|
|||
<translation>Disable Community Rating for Episodes</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</source>
|
||||
<translation>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</translation>
|
||||
<source>Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.</source>
|
||||
<translation>Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Configure the maximum playback bitrate.</source>
|
||||
<translation>Configure the maximum playback bitrate.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Biographical information for this person is not currently available.</source>
|
||||
<translation>Biographical information for this person is not currently available.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Playback Bitrate Limits</source>
|
||||
<translation>Playback Bitrate Limits</translation>
|
||||
<source>Enable Limit</source>
|
||||
<translation>Enable Limit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Set limits for how high playback bitrates are allowed to be.</source>
|
||||
<translation>Set limits for how high playback bitrates are allowed to be.</translation>
|
||||
<source>Enable or disable the 'Maximum Bitrate' setting.</source>
|
||||
<translation>Enable or disable the 'Maximum Bitrate' setting.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Limits Enabled</source>
|
||||
<translation>Limits Enabled</translation>
|
||||
<source>Bitrate Limit</source>
|
||||
<translation>Bitrate Limit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.</source>
|
||||
<translation>If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.</translation>
|
||||
<source>Maximum Bitrate</source>
|
||||
<translation>Maximum Bitrate</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Playback Bitrate Limit</source>
|
||||
<translation>Playback Bitrate Limit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.</source>
|
||||
<translation>Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.</translation>
|
||||
<source>Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.</source>
|
||||
<translation>Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Libraries</source>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
manifest
5
manifest
|
@ -3,7 +3,7 @@
|
|||
title=Jellyfin
|
||||
major_version=1
|
||||
minor_version=6
|
||||
build_version=4
|
||||
build_version=5
|
||||
|
||||
### Main Menu Icons / Channel Poster Artwork
|
||||
|
||||
|
@ -21,7 +21,6 @@ splash_min_time=1500
|
|||
|
||||
ui_resolutions=fhd
|
||||
|
||||
screensaver_private=1
|
||||
screensaver_title=Jellyfin
|
||||
confirm_partner_button=1
|
||||
|
||||
supports_input_launch=1
|
||||
|
|
5969
package-lock.json
generated
5969
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
|
@ -1,30 +1,43 @@
|
|||
{
|
||||
"name": "jellyfin-roku",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"description": "Roku app for Jellyfin media server",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"api": "npm:jellyfin-api-bs-client@1.0.5",
|
||||
"bgv": "npm:button-group-vert@1.0.2",
|
||||
"brighterscript-formatter": "1.6.24",
|
||||
"intKeyboard": "npm:integer-keyboard@1.0.12",
|
||||
"sob": "npm:slide-out-button@1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rokucommunity/bslint": "0.8.2",
|
||||
"brighterscript": "0.62.0",
|
||||
"ropm": "0.10.12",
|
||||
"jshint": "^2.13.6",
|
||||
"markdownlint-cli2": "0.6.0",
|
||||
"spellchecker-cli": "6.1.1"
|
||||
"@rokucommunity/bslint": "0.8.3",
|
||||
"brighterscript": "0.64.2",
|
||||
"bslib": "npm:@rokucommunity/bslib@0.1.1",
|
||||
"jshint": "2.13.6",
|
||||
"markdownlint-cli2": "0.7.0",
|
||||
"rimraf": "4.4.1",
|
||||
"roku-deploy": "3.10.0",
|
||||
"roku-log-bsc-plugin": "0.7.0",
|
||||
"rooibos-roku": "5.4.2",
|
||||
"ropm": "0.10.13",
|
||||
"spellchecker-cli": "6.1.1",
|
||||
"undent": "0.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "npx ropm copy",
|
||||
"validate": "npx bsc --copy-to-staging=false --create-package=false",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build-tests": "npx rimraf build/ && npx bsc --project bsconfig-tests.json",
|
||||
"build-tdd": "npx rimraf build/ && npx bsc --project bsconfig-tdd.json",
|
||||
"check-formatting": "npx bsfmt --check",
|
||||
"format": "npx bsfmt --write",
|
||||
"lint": "bslint",
|
||||
"lint-json": "jshint --extra-ext .json --verbose --exclude node_modules ./",
|
||||
"lint-markdown": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
|
||||
"lint-spelling": "spellchecker -d dictionary.txt --files \"**/*.md\" \"**/.*/**/*.md\" \"!node_modules/**/*.md\"",
|
||||
"check-formatting": "npx bsfmt --check",
|
||||
"format": "npx bsfmt --write"
|
||||
"postinstall": "npx ropm copy",
|
||||
"validate": "npx bsc --copy-to-staging=false --create-package=false"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jellyfin/jellyfin-roku.git"
|
||||
"url": "https://github.com/jellyfin/jellyfin-roku.git"
|
||||
},
|
||||
"keywords": [
|
||||
"jellyfin",
|
||||
|
@ -35,12 +48,5 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/jellyfin/jellyfin-roku/issues"
|
||||
},
|
||||
"homepage": "https://github.com/jellyfin/jellyfin-roku#readme",
|
||||
"dependencies": {
|
||||
"api": "npm:jellyfin-api-bs-client@^1.0.5",
|
||||
"bgv": "npm:button-group-vert@^1.0.2",
|
||||
"brighterscript-formatter": "^1.6.8",
|
||||
"sob": "npm:slide-out-button@^1.0.1",
|
||||
"intKeyboard": "npm:integer-keyboard@^1.0.12"
|
||||
}
|
||||
"homepage": "https://github.com/jellyfin/jellyfin-roku#readme"
|
||||
}
|
|
@ -3,9 +3,29 @@
|
|||
"title": "Playback",
|
||||
"description": "Settings relating to playback and supported codec and media types.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Bitrate Limit",
|
||||
"description": "Configure the maximum playback bitrate.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Enable Limit",
|
||||
"description": "Enable or disable the 'Maximum Bitrate' setting.",
|
||||
"settingName": "playback.bitrate.maxlimited",
|
||||
"type": "bool",
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"title": "Maximum Bitrate",
|
||||
"description": "Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.",
|
||||
"settingName": "playback.bitrate.limit",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Codec Support",
|
||||
"description": "Enable or disable Direct Play support for certain codecs",
|
||||
"description": "Enable or disable Direct Play support for certain codecs.",
|
||||
"children": [
|
||||
{
|
||||
"title": "AV1",
|
||||
|
@ -30,26 +50,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Playback Bitrate Limits",
|
||||
"description": "Set limits for how high playback bitrates are allowed to be.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Limits Enabled",
|
||||
"description": "If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.",
|
||||
"settingName": "playback.bitrate.maxlimited",
|
||||
"type": "bool",
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"title": "Playback Bitrate Limit",
|
||||
"description": "Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.",
|
||||
"settingName": "playback.bitrate.limit",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Profile Level Support",
|
||||
"description": "Attempt Direct Play of potentially unsupported profile levels",
|
||||
|
@ -72,7 +72,7 @@
|
|||
},
|
||||
{
|
||||
"title": "Cinema Mode",
|
||||
"description": "Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.",
|
||||
"description": "Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.",
|
||||
"settingName": "playback.cinemamode",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
|
@ -110,7 +110,7 @@
|
|||
"children": [
|
||||
{
|
||||
"title": "Hide Clock",
|
||||
"description": "Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.",
|
||||
"description": "Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.",
|
||||
"settingName": "ui.design.hideclock",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
|
@ -145,19 +145,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Screensaver",
|
||||
"description": "Options for Jellyfin's screensaver.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Use Splashscreen as Screensaver Background",
|
||||
"description": "Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.",
|
||||
"settingName": "ui.screensaver.splashBackground",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Libraries",
|
||||
"description": "Settings relating to the appearance of Library pages.",
|
||||
|
@ -245,14 +232,14 @@
|
|||
"children": [
|
||||
{
|
||||
"title": "Blur Unwatched Episodes",
|
||||
"description": "If enabled, images of unwatched episodes will be blurred.",
|
||||
"description": "Blur images of unwatched episodes.",
|
||||
"settingName": "ui.tvshows.blurunwatched",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"title": "Disable Community Rating for Episodes",
|
||||
"description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
|
||||
"description": "Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.",
|
||||
"settingName": "ui.tvshows.disableCommunityRating",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
|
|
226
source/Main.brs
226
source/Main.brs
|
@ -1,26 +1,6 @@
|
|||
sub Main (args as dynamic) as void
|
||||
|
||||
appInfo = CreateObject("roAppInfo")
|
||||
|
||||
if appInfo.IsDev() and args.RunTests = "true" and TF_Utils__IsFunction(TestRunner)
|
||||
' POST to {ROKU ADDRESS}:8060/launch/dev?RunTests=true
|
||||
Runner = TestRunner()
|
||||
|
||||
Runner.SetFunctions([
|
||||
TestSuite__Misc
|
||||
])
|
||||
|
||||
Runner.Logger.SetVerbosity(1)
|
||||
Runner.Logger.SetEcho(false)
|
||||
Runner.Logger.SetJUnit(false)
|
||||
Runner.SetFailFast(true)
|
||||
|
||||
Runner.Run()
|
||||
end if
|
||||
|
||||
' The main function that runs when the application is launched.
|
||||
m.screen = CreateObject("roSGScreen")
|
||||
|
||||
' Set global constants
|
||||
setConstants()
|
||||
' Write screen tracker for screensaver
|
||||
|
@ -30,7 +10,7 @@ sub Main (args as dynamic) as void
|
|||
m.port = CreateObject("roMessagePort")
|
||||
m.screen.setMessagePort(m.port)
|
||||
m.scene = m.screen.CreateScene("JFScene")
|
||||
m.screen.show()
|
||||
m.screen.show() ' vscode_rale_tracker_entry
|
||||
|
||||
' Set any initial Global Variables
|
||||
m.global = m.screen.getGlobalNode()
|
||||
|
@ -96,13 +76,19 @@ sub Main (args as dynamic) as void
|
|||
m.scene.observeField("exit", m.port)
|
||||
|
||||
' Downloads and stores a fallback font to tmp:/
|
||||
if parseJSON(APIRequest("/System/Configuration/encoding").GetToString())["EnableFallbackFont"] = true
|
||||
re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
|
||||
filename = APIRequest("FallbackFont/Fonts").GetToString()
|
||||
filename = re.match(filename)
|
||||
if filename.count() > 0
|
||||
filename = filename[1]
|
||||
APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
|
||||
configEncoding = api_API().system.getconfigurationbyname("encoding")
|
||||
|
||||
if isValid(configEncoding) and isValid(configEncoding.EnableFallbackFont)
|
||||
if configEncoding.EnableFallbackFont
|
||||
re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
|
||||
filename = APIRequest("FallbackFont/Fonts").GetToString()
|
||||
if isValid(filename)
|
||||
filename = re.match(filename)
|
||||
if isValid(filename) and filename.count() > 0
|
||||
filename = filename[1]
|
||||
APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
|
@ -182,8 +168,6 @@ sub Main (args as dynamic) as void
|
|||
group.lastFocus.setFocus(true)
|
||||
end if
|
||||
end if
|
||||
|
||||
reportingNode.quickPlayNode.type = ""
|
||||
end if
|
||||
else if isNodeEvent(msg, "selectedItem")
|
||||
' If you select a library from ANYWHERE, follow this flow
|
||||
|
@ -191,7 +175,7 @@ sub Main (args as dynamic) as void
|
|||
|
||||
m.selectedItemType = selectedItem.type
|
||||
|
||||
if selectedItem.type = "CollectionFolder"
|
||||
if selectedItem.type = "CollectionFolder" or selectedItem.type = "BoxSet"
|
||||
if selectedItem.collectionType = "movies"
|
||||
group = CreateMovieLibraryView(selectedItem)
|
||||
else if selectedItem.collectionType = "music"
|
||||
|
@ -276,6 +260,7 @@ sub Main (args as dynamic) as void
|
|||
group = CreatePlaylistView(selectedItem.json)
|
||||
else if selectedItem.type = "Audio"
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("push", selectedItem.json)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else
|
||||
|
@ -317,6 +302,7 @@ sub Main (args as dynamic) as void
|
|||
screenContent = msg.getRoSGNode()
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else if isNodeEvent(msg, "playItem")
|
||||
|
@ -325,6 +311,7 @@ sub Main (args as dynamic) as void
|
|||
screenContent = msg.getRoSGNode()
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else if isNodeEvent(msg, "playAllSelected")
|
||||
|
@ -334,6 +321,7 @@ sub Main (args as dynamic) as void
|
|||
m.spinner.visible = true
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("set", screenContent.albumData.items)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else if isNodeEvent(msg, "playArtistSelected")
|
||||
|
@ -341,6 +329,7 @@ sub Main (args as dynamic) as void
|
|||
screenContent = msg.getRoSGNode()
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("set", CreateArtistMix(screenContent.pageContent.id).Items)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
|
||||
|
@ -360,6 +349,7 @@ sub Main (args as dynamic) as void
|
|||
if isValid(screenContent.albumData.items)
|
||||
if screenContent.albumData.items.count() > 0
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.albumData.items[0].id).Items)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
|
||||
|
@ -371,6 +361,7 @@ sub Main (args as dynamic) as void
|
|||
if not viewHandled
|
||||
' Create instant mix based on selected artist
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.pageContent.id).Items)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
end if
|
||||
|
@ -418,6 +409,7 @@ sub Main (args as dynamic) as void
|
|||
group = CreateAlbumView(node.json)
|
||||
else if node.type = "Audio"
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("push", node.json)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else if node.type = "Person"
|
||||
|
@ -432,6 +424,7 @@ sub Main (args as dynamic) as void
|
|||
selectedIndex = msg.getData()
|
||||
screenContent = msg.getRoSGNode()
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("resetShuffle")
|
||||
m.global.queueManager.callFunc("push", screenContent.albumData.items[node.id])
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else
|
||||
|
@ -651,174 +644,3 @@ sub Main (args as dynamic) as void
|
|||
end while
|
||||
|
||||
end sub
|
||||
|
||||
function LoginFlow(startOver = false as boolean)
|
||||
'Collect Jellyfin server and user information
|
||||
start_login:
|
||||
|
||||
if get_setting("server") = invalid then startOver = true
|
||||
|
||||
invalidServer = true
|
||||
if not startOver
|
||||
' Show Connecting to Server spinner
|
||||
dialog = createObject("roSGNode", "ProgressDialog")
|
||||
dialog.title = tr("Connecting to Server")
|
||||
m.scene.dialog = dialog
|
||||
invalidServer = ServerInfo().Error
|
||||
dialog.close = true
|
||||
end if
|
||||
|
||||
m.serverSelection = "Saved"
|
||||
if startOver or invalidServer
|
||||
print "Get server details"
|
||||
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
|
||||
m.serverSelection = CreateServerGroup()
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
if m.serverSelection = "backPressed"
|
||||
print "backPressed"
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
return false
|
||||
end if
|
||||
SaveServerList()
|
||||
end if
|
||||
|
||||
if get_setting("active_user") = invalid
|
||||
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
|
||||
publicUsers = GetPublicUsers()
|
||||
if publicUsers.count()
|
||||
publicUsersNodes = []
|
||||
for each item in publicUsers
|
||||
user = CreateObject("roSGNode", "PublicUserData")
|
||||
user.id = item.Id
|
||||
user.name = item.Name
|
||||
if item.PrimaryImageTag <> invalid
|
||||
user.ImageURL = UserImageURL(user.id, { "tag": item.PrimaryImageTag })
|
||||
end if
|
||||
publicUsersNodes.push(user)
|
||||
end for
|
||||
userSelected = CreateUserSelectGroup(publicUsersNodes)
|
||||
if userSelected = "backPressed"
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
return LoginFlow(true)
|
||||
else
|
||||
'Try to login without password. If the token is valid, we're done
|
||||
get_token(userSelected, "")
|
||||
if get_setting("active_user") <> invalid
|
||||
m.user = AboutMe()
|
||||
LoadUserPreferences()
|
||||
LoadUserAbilities(m.user)
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
else
|
||||
userSelected = ""
|
||||
end if
|
||||
passwordEntry = CreateSigninGroup(userSelected)
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
if passwordEntry = "backPressed"
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
return LoginFlow(true)
|
||||
end if
|
||||
end if
|
||||
|
||||
m.user = AboutMe()
|
||||
if m.user = invalid or m.user.id <> get_setting("active_user")
|
||||
print "Login failed, restart flow"
|
||||
unset_setting("active_user")
|
||||
goto start_login
|
||||
end if
|
||||
|
||||
LoadUserPreferences()
|
||||
LoadUserAbilities(m.user)
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
|
||||
'Send Device Profile information to server
|
||||
body = getDeviceCapabilities()
|
||||
req = APIRequest("/Sessions/Capabilities/Full")
|
||||
req.SetRequest("POST")
|
||||
postJson(req, FormatJson(body))
|
||||
return true
|
||||
end function
|
||||
|
||||
sub SaveServerList()
|
||||
'Save off this server to our list of saved servers for easier navigation between servers
|
||||
server = get_setting("server")
|
||||
saved = get_setting("saved_servers")
|
||||
if server <> invalid
|
||||
server = LCase(server)'Saved server data is always lowercase
|
||||
end if
|
||||
entryCount = 0
|
||||
addNewEntry = true
|
||||
savedServers = { serverList: [] }
|
||||
if saved <> invalid
|
||||
savedServers = ParseJson(saved)
|
||||
entryCount = savedServers.serverList.Count()
|
||||
if savedServers.serverList <> invalid and entryCount > 0
|
||||
for each item in savedServers.serverList
|
||||
if item.baseUrl = server
|
||||
addNewEntry = false
|
||||
exit for
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
end if
|
||||
|
||||
if addNewEntry
|
||||
if entryCount = 0
|
||||
set_setting("saved_servers", FormatJson({ serverList: [{ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] }))
|
||||
else
|
||||
savedServers.serverList.Push({ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 })
|
||||
set_setting("saved_servers", FormatJson(savedServers))
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub DeleteFromServerList(urlToDelete)
|
||||
saved = get_setting("saved_servers")
|
||||
if urlToDelete <> invalid
|
||||
urlToDelete = LCase(urlToDelete)
|
||||
end if
|
||||
if saved <> invalid
|
||||
savedServers = ParseJson(saved)
|
||||
newServers = { serverList: [] }
|
||||
for each item in savedServers.serverList
|
||||
if item.baseUrl <> urlToDelete
|
||||
newServers.serverList.Push(item)
|
||||
end if
|
||||
end for
|
||||
set_setting("saved_servers", FormatJson(newServers))
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub RunScreenSaver()
|
||||
print "Starting screensaver..."
|
||||
|
||||
scene = ReadAsciiFile("tmp:/scene")
|
||||
if scene = "nowplaying" then return
|
||||
|
||||
screen = createObject("roSGScreen")
|
||||
m.port = createObject("roMessagePort")
|
||||
screen.setMessagePort(m.port)
|
||||
|
||||
screen.createScene("Screensaver")
|
||||
screen.Show()
|
||||
|
||||
while true
|
||||
msg = wait(8000, m.port)
|
||||
if msg <> invalid
|
||||
msgType = type(msg)
|
||||
if msgType = "roSGScreenEvent"
|
||||
if msg.isScreenClosed() then return
|
||||
end if
|
||||
end if
|
||||
end while
|
||||
|
||||
end sub
|
||||
|
||||
' Roku Performance monitoring
|
||||
sub SendPerformanceBeacon(signalName as string)
|
||||
if m.global.app_loaded = false
|
||||
m.scene.signalBeacon(signalName)
|
||||
end if
|
||||
end sub
|
||||
|
|
|
@ -1,3 +1,149 @@
|
|||
function LoginFlow(startOver = false as boolean)
|
||||
'Collect Jellyfin server and user information
|
||||
start_login:
|
||||
|
||||
if get_setting("server") = invalid then startOver = true
|
||||
|
||||
invalidServer = true
|
||||
if not startOver
|
||||
' Show Connecting to Server spinner
|
||||
dialog = createObject("roSGNode", "ProgressDialog")
|
||||
dialog.title = tr("Connecting to Server")
|
||||
m.scene.dialog = dialog
|
||||
invalidServer = ServerInfo().Error
|
||||
dialog.close = true
|
||||
end if
|
||||
|
||||
m.serverSelection = "Saved"
|
||||
if startOver or invalidServer
|
||||
print "Get server details"
|
||||
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
|
||||
m.serverSelection = CreateServerGroup()
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
if m.serverSelection = "backPressed"
|
||||
print "backPressed"
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
return false
|
||||
end if
|
||||
SaveServerList()
|
||||
end if
|
||||
|
||||
if get_setting("active_user") = invalid
|
||||
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
|
||||
publicUsers = GetPublicUsers()
|
||||
if publicUsers.count()
|
||||
publicUsersNodes = []
|
||||
for each item in publicUsers
|
||||
user = CreateObject("roSGNode", "PublicUserData")
|
||||
user.id = item.Id
|
||||
user.name = item.Name
|
||||
if item.PrimaryImageTag <> invalid
|
||||
user.ImageURL = UserImageURL(user.id, { "tag": item.PrimaryImageTag })
|
||||
end if
|
||||
publicUsersNodes.push(user)
|
||||
end for
|
||||
userSelected = CreateUserSelectGroup(publicUsersNodes)
|
||||
if userSelected = "backPressed"
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
return LoginFlow(true)
|
||||
else
|
||||
'Try to login without password. If the token is valid, we're done
|
||||
get_token(userSelected, "")
|
||||
if get_setting("active_user") <> invalid
|
||||
m.user = AboutMe()
|
||||
LoadUserPreferences()
|
||||
LoadUserAbilities(m.user)
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
else
|
||||
userSelected = ""
|
||||
end if
|
||||
passwordEntry = CreateSigninGroup(userSelected)
|
||||
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
||||
if passwordEntry = "backPressed"
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
return LoginFlow(true)
|
||||
end if
|
||||
end if
|
||||
|
||||
m.user = AboutMe()
|
||||
if m.user = invalid or m.user.id <> get_setting("active_user")
|
||||
print "Login failed, restart flow"
|
||||
unset_setting("active_user")
|
||||
goto start_login
|
||||
end if
|
||||
|
||||
LoadUserPreferences()
|
||||
LoadUserAbilities(m.user)
|
||||
m.global.sceneManager.callFunc("clearScenes")
|
||||
|
||||
'Send Device Profile information to server
|
||||
body = getDeviceCapabilities()
|
||||
req = APIRequest("/Sessions/Capabilities/Full")
|
||||
req.SetRequest("POST")
|
||||
postJson(req, FormatJson(body))
|
||||
return true
|
||||
end function
|
||||
|
||||
sub SaveServerList()
|
||||
'Save off this server to our list of saved servers for easier navigation between servers
|
||||
server = get_setting("server")
|
||||
saved = get_setting("saved_servers")
|
||||
if server <> invalid
|
||||
server = LCase(server)'Saved server data is always lowercase
|
||||
end if
|
||||
entryCount = 0
|
||||
addNewEntry = true
|
||||
savedServers = { serverList: [] }
|
||||
if saved <> invalid
|
||||
savedServers = ParseJson(saved)
|
||||
entryCount = savedServers.serverList.Count()
|
||||
if savedServers.serverList <> invalid and entryCount > 0
|
||||
for each item in savedServers.serverList
|
||||
if item.baseUrl = server
|
||||
addNewEntry = false
|
||||
exit for
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
end if
|
||||
|
||||
if addNewEntry
|
||||
if entryCount = 0
|
||||
set_setting("saved_servers", FormatJson({ serverList: [{ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] }))
|
||||
else
|
||||
savedServers.serverList.Push({ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 })
|
||||
set_setting("saved_servers", FormatJson(savedServers))
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub DeleteFromServerList(urlToDelete)
|
||||
saved = get_setting("saved_servers")
|
||||
if urlToDelete <> invalid
|
||||
urlToDelete = LCase(urlToDelete)
|
||||
end if
|
||||
if saved <> invalid
|
||||
savedServers = ParseJson(saved)
|
||||
newServers = { serverList: [] }
|
||||
for each item in savedServers.serverList
|
||||
if item.baseUrl <> urlToDelete
|
||||
newServers.serverList.Push(item)
|
||||
end if
|
||||
end for
|
||||
set_setting("saved_servers", FormatJson(newServers))
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Roku Performance monitoring
|
||||
sub SendPerformanceBeacon(signalName as string)
|
||||
if m.global.app_loaded = false
|
||||
m.scene.signalBeacon(signalName)
|
||||
end if
|
||||
end sub
|
||||
|
||||
function CreateServerGroup()
|
||||
screen = CreateObject("roSGNode", "SetServerScreen")
|
||||
screen.optionsAvailable = true
|
||||
|
@ -542,6 +688,9 @@ function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_
|
|||
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog)
|
||||
|
||||
if video = invalid then return invalid
|
||||
|
||||
video.allowCaptions = true
|
||||
|
||||
if video.errorMsg = "introaborted" then return video
|
||||
video.observeField("selectSubtitlePressed", m.port)
|
||||
video.observeField("selectPlaybackInfoPressed", m.port)
|
||||
|
|
|
@ -47,9 +47,6 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
|
|||
end if
|
||||
|
||||
if m.videotype = "Episode" or m.videotype = "Series"
|
||||
if isValid(meta.json) and isValid(meta.json.RunTimeTicks)
|
||||
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
|
||||
end if
|
||||
video.content.contenttype = "episode"
|
||||
end if
|
||||
|
||||
|
@ -332,6 +329,9 @@ function PlayIntroVideo(video_id, audio_stream_idx) as boolean
|
|||
if lcase(introVideos.items[0].name) = "rick roll'd" then return true
|
||||
|
||||
introVideo = VideoPlayer(introVideos.items[0].id, introVideos.items[0].id, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), false, false)
|
||||
if isValid(introVideo)
|
||||
introVideo.allowCaptions = false
|
||||
end if
|
||||
|
||||
port = CreateObject("roMessagePort")
|
||||
introVideo.observeField("state", port)
|
||||
|
|
|
@ -1,146 +1,50 @@
|
|||
[
|
||||
{
|
||||
"description": "Feature: Phase 1 CJK subtitle support - external files only",
|
||||
"author": "jkim2492"
|
||||
"description": "Bug Fix: Rows on home view not refreshing when data has changed",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Fix multiple client crashes identified by crashlogs",
|
||||
"description": "Bug Fix: Next up section does not populate correctly after watching a TV episode",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Next episode button does not display consistently",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Crash when fallback font API call fails",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Core: Fix Jellyfin links in readme",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Feature: Add TV series & season shuffle",
|
||||
"description": "Bug Fix: Music shuffle not working",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Show \"Actor\" when an actor has no role",
|
||||
"description": "Updated View: Add current playback time and song length to the sides of the audio progress bar",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Turn off loop mode when user manually changes song",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Core: Enable confirm partner button and setup RALE",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Feature: Phase 1 playlist support",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Fix Typo: trancoding -> transcoding",
|
||||
"author": "RussianCow"
|
||||
},
|
||||
{
|
||||
"description": "Feature: Create global audio player",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Core: Add user policy to check if canDelete",
|
||||
"author": "candry7731"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Only show next episode button if \"Play next episode automatically\" setting is enabled in web client",
|
||||
"description": "Bug Fix: Clock not displaying on login view",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "New Setting: Disable unwatched episode count",
|
||||
"author": "1hitsong"
|
||||
"description": "Core: Add build badge and rework readme content",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "New Setting: Next episode button time",
|
||||
"author": "candry7731"
|
||||
},
|
||||
{
|
||||
"description": "New View: New persondetails view",
|
||||
"description": "Core: Adjust settings to follow latest guidelines",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Make title scrolling consistent in extras slider",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Feature: Make pressing the options button close settings menu",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Graceful episode playback failure",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Fix default view setting for movie genres",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Core: Update CI ubuntu version and node version",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "New Setting: Custom max video bitrate",
|
||||
"author": "jimdogx"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Updated \"OnNow\" home row to default to channel images if program images are not availible",
|
||||
"author": "candry7731"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Improve settings menu, implement title hover and hide in missing locations",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Make home view load faster",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Fix option menu focus if opened while library still loading",
|
||||
"author": "ApexArray"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Add Genres, Parental Ratings, and Years as movie filters",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Bug Fix: Revert change that removed image cache busting",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Fix distorted TV episode posters. Add client-side progress bar and played indicator.",
|
||||
"author": "ApexArray"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Improve quality of album art on now playing view",
|
||||
"author": "1hitsong"
|
||||
},
|
||||
{
|
||||
"description": "Updated View: Loading spinner, Progress Dialog and movie details button animate",
|
||||
"author": "candry7731"
|
||||
},
|
||||
{
|
||||
"description": "Core: Add settings guidelines to Dev Guide",
|
||||
"author": "sevenrats"
|
||||
},
|
||||
{
|
||||
"description": "Core: Add workflow to validate language translation files",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Remove optional chaining operators from code",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Make CI throw error for duplicate translation entries",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Fix en_US translation file",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Add a production build workflow",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Add json and markdown to lint workflow + add automation workflow",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Make workflows use latest LTS release + cache npm",
|
||||
"author": "cewert"
|
||||
},
|
||||
{
|
||||
"description": "Core: Install & Configure code unit test suite",
|
||||
"author": "1hitsong"
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
|
@ -1,81 +0,0 @@
|
|||
function TestSuite__Misc() as object
|
||||
|
||||
' Inherite test suite from BaseTestSuite
|
||||
this = BaseTestSuite()
|
||||
|
||||
' Test suite name for log statistics
|
||||
this.Name = "MiscTestSuite"
|
||||
|
||||
this.SetUp = MiscTestSuite__SetUp
|
||||
this.TearDown = MiscTestSuite__TearDown
|
||||
|
||||
' Add tests to suite's tests collection
|
||||
this.addTest("IsValid() true", TestCase__Misc_IsValid_True)
|
||||
this.addTest("IsValid() false", TestCase__Misc_IsValid_False)
|
||||
this.addTest("RoundNumber() Floor", TestCase__Misc_RoundNumber_Floor)
|
||||
this.addTest("RoundNumber() Ceiling", TestCase__Misc_RoundNumber_Ceiling)
|
||||
|
||||
return this
|
||||
end function
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' This function called immediately before running tests of current suite.
|
||||
'----------------------------------------------------------------
|
||||
sub MiscTestSuite__SetUp()
|
||||
end sub
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' This function called immediately after running tests of current suite.
|
||||
'----------------------------------------------------------------
|
||||
sub MiscTestSuite__TearDown()
|
||||
end sub
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' Check if isValid() properly identifies valid items
|
||||
'
|
||||
' @return An empty string if test is success or error message if not.
|
||||
'----------------------------------------------------------------
|
||||
function TestCase__Misc_IsValid_True() as string
|
||||
returnResults = ""
|
||||
testData = [1, 2, [3, 4], { "key": invalid }, [1, 2, 3], CreateObject("roAppInfo")]
|
||||
|
||||
for each testItem in testData
|
||||
returnResults = returnResults + m.AssertTrue(isValid(testItem))
|
||||
end for
|
||||
|
||||
return m.AssertEmpty(returnResults)
|
||||
end function
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' Check if isValid() properly identifies invalid items
|
||||
'
|
||||
' @return An empty string if test is success or error message if not.
|
||||
'----------------------------------------------------------------
|
||||
function TestCase__Misc_IsValid_False() as string
|
||||
returnResults = ""
|
||||
testData = [invalid, CreateObject("nothing")]
|
||||
|
||||
for each testItem in testData
|
||||
returnResults = m.AssertFalse(isValid(testItem))
|
||||
end for
|
||||
|
||||
return m.AssertEmpty(returnResults)
|
||||
end function
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' Check if roundNumber() properly rounds down
|
||||
'
|
||||
' @return An empty string if test is success or error message if not.
|
||||
'----------------------------------------------------------------
|
||||
function TestCase__Misc_RoundNumber_Floor() as string
|
||||
return m.AssertEqual(roundNumber(9.4), 9)
|
||||
end function
|
||||
|
||||
'----------------------------------------------------------------
|
||||
' Check if roundNumber() properly rounds up
|
||||
'
|
||||
' @return An empty string if test is success or error message if not.
|
||||
'----------------------------------------------------------------
|
||||
function TestCase__Misc_RoundNumber_Ceiling() as string
|
||||
return m.AssertEqual(roundNumber(9.6), 10)
|
||||
end function
|
|
@ -41,6 +41,18 @@ function ticksToHuman(ticks as longinteger) as string
|
|||
return r
|
||||
end function
|
||||
|
||||
function secondsToHuman(totalSeconds as integer) as string
|
||||
hours = stri(int(totalSeconds / 3600)).trim()
|
||||
minutes = stri(int((totalSeconds - (val(hours) * 3600)) / 60)).trim()
|
||||
seconds = stri(totalSeconds - (val(hours) * 3600) - (val(minutes) * 60)).trim()
|
||||
if val(hours) > 0 and val(minutes) < 10 then minutes = "0" + minutes
|
||||
if val(seconds) < 10 then seconds = "0" + seconds
|
||||
r = ""
|
||||
if val(hours) > 0 then r = hours + ":"
|
||||
r = r + minutes + ":" + seconds
|
||||
return r
|
||||
end function
|
||||
|
||||
' Format time as 12 or 24 hour format based on system clock setting
|
||||
function formatTime(time) as string
|
||||
hours = time.getHours()
|
||||
|
@ -194,13 +206,13 @@ sub setFieldTextValue(field, value)
|
|||
end sub
|
||||
|
||||
' Returns whether or not passed value is valid
|
||||
function isValid(input) as boolean
|
||||
function isValid(input as dynamic) as boolean
|
||||
return input <> invalid
|
||||
end function
|
||||
|
||||
' Returns whether or not passed value is valid and not empty
|
||||
' Accepts a string, or any countable type (arrays and lists)
|
||||
function isValidAndNotEmpty(input) as boolean
|
||||
function isValidAndNotEmpty(input as dynamic) as boolean
|
||||
if not isValid(input) then return false
|
||||
' Use roAssociativeArray instead of list so we get access to the doesExist() method
|
||||
countableTypes = { "array": 1, "list": 1, "roarray": 1, "roassociativearray": 1, "rolist": 1 }
|
||||
|
|
4
test-app/manifest
Normal file
4
test-app/manifest
Normal file
|
@ -0,0 +1,4 @@
|
|||
title=Rooibos Unit Testing
|
||||
major_version=0
|
||||
minor_version=1
|
||||
build_version=1
|
18
test-app/source/BaseTestSuite.spec.bs
Normal file
18
test-app/source/BaseTestSuite.spec.bs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace tests
|
||||
|
||||
class BaseTestSuite extends rooibos.BaseTestSuite
|
||||
private appController
|
||||
|
||||
protected override function setup()
|
||||
'Do something here all your files need like setup the logger, etc
|
||||
end function
|
||||
|
||||
protected override function beforeEach()
|
||||
'do things here that all your tests need
|
||||
end function
|
||||
|
||||
protected override function afterEach()
|
||||
'tidy things up
|
||||
end function
|
||||
end class
|
||||
end namespace
|
4
test-app/source/Main.bs
Normal file
4
test-app/source/Main.bs
Normal file
|
@ -0,0 +1,4 @@
|
|||
function Main(args)
|
||||
? "here is my code"
|
||||
? "hello"
|
||||
end function
|
188
test-app/source/tests/utils/misc/isValid.spec.bs
Normal file
188
test-app/source/tests/utils/misc/isValid.spec.bs
Normal file
|
@ -0,0 +1,188 @@
|
|||
namespace tests
|
||||
@suite("isValid functions")
|
||||
class isValidTests extends tests.BaseTestSuite
|
||||
|
||||
protected override function setup()
|
||||
super.setup()
|
||||
m.myArray = CreateObject("roArray", 3, true)
|
||||
m.myAssArray = { one: invalid, two: "invalid", three: 123.456 }
|
||||
m.myEmptyArray = CreateObject("roArray", 0, false)
|
||||
m.myEmptyList = CreateObject("roList")
|
||||
m.myList = CreateObject("roList")
|
||||
m.myList.AddTail("string")
|
||||
end function
|
||||
|
||||
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
@describe("isValid()")
|
||||
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
@it("works with booleans")
|
||||
@params(true, true)
|
||||
@params(false, true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with integers")
|
||||
@params(-1234567890, true)
|
||||
@params(0, true)
|
||||
@params(1234567890, true)
|
||||
@params(1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890, true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with floats")
|
||||
@params(-12.3456789, true)
|
||||
@params(12.3456789, true)
|
||||
@params(1.23456E+30, true)
|
||||
@params(12.3456789!, true)
|
||||
@params(123456789012345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678901234567890, true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with strings")
|
||||
@params("", true)
|
||||
@params(" ", true)
|
||||
@params("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Augue neque gravida in fermentum et. Eget lorem dolor sed viverra ipsum nunc. At quis risus sed vulputate odio ut enim. Ultricies integer quis auctor elit sed. Egestas congue quisque egestas diam in. Aliquam sem fringilla ut morbi tincidunt. Malesuada bibendum arcu vitae elementum curabitur. Aliquet sagittis id consectetur purus ut faucibus pulvinar. Eget gravida cum sociis natoque. Sollicitudin aliquam ultrices sagittis orci. Ut etiam sit amet nisl purus. Luctus venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Vitae ultricies leo integer malesuada nunc. Vitae ultricies leo integer malesuada nunc vel risus commodo. Luctus accumsan tortor posuere ac ut. Urna cursus eget nunc scelerisque viverra mauris in. Accumsan sit amet nulla facilisi morbi tempus iaculis urna id. Mauris vitae ultricies leo integer malesuada nunc vel risus commodo. Morbi tincidunt augue interdum velit euismod in pellentesque.", true)
|
||||
@params("~!@#$%^&*()_-+=`\|]}';:.,/?", true)
|
||||
@params("true", true)
|
||||
@params("false", true)
|
||||
@params("invalid", true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with arrays")
|
||||
@params([0, 1, 2, 3, 4, 5], true)
|
||||
@params(["invalid", "one", "two", "three", "four", "five"], true)
|
||||
@params([invalid, invalid, invalid], true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with associative arrays")
|
||||
@params({ myInteger: 1, myString: "one", myInvalid: invalid, myEmptyString: "" }, true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with an array of associative arrays")
|
||||
@params(
|
||||
[
|
||||
{
|
||||
Title: "The Notebook",
|
||||
releaseDate: "2000"
|
||||
},
|
||||
{
|
||||
Title: "Caddyshack",
|
||||
releaseDate: "1976"
|
||||
}
|
||||
], true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works when accessing arrays")
|
||||
function _()
|
||||
m.assertEqual(isValid(m.myAssArray.one), false)
|
||||
m.assertEqual(isValid(m.myAssArray.two), true)
|
||||
end function
|
||||
|
||||
@it("works when accessing an invalid array index")
|
||||
function _()
|
||||
m.assertEqual(isValid(m.myAssArray.zero), false)
|
||||
end function
|
||||
|
||||
@it("works with invalid")
|
||||
@params(invalid, false)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with nodes")
|
||||
@params("#RBSNode", true)
|
||||
@params("#RBSNode|Group", true)
|
||||
@params("#RBSNode|Label", true)
|
||||
@params("#RBSNode|ScrollingLabel", true)
|
||||
@params("#RBSNode|Poster", true)
|
||||
@params("#RBSNode|Rectangle", true)
|
||||
@params("#RBSNode|Font", true)
|
||||
@params("#RBSNode|Button", true)
|
||||
@params("#RBSNode|Rectangle", true)
|
||||
@params("#RBSNode|Overhang", true)
|
||||
@params("#RBSNode|Audio", true)
|
||||
@params("#RBSNode|Video", true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValid(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with objects")
|
||||
function _()
|
||||
myList = CreateObject("roList")
|
||||
myLongInteger = CreateObject("roLongInteger")
|
||||
myDouble = CreateObject("roDouble")
|
||||
myFloat = CreateObject("roFloat")
|
||||
myInvalid = CreateObject("roInvalid")
|
||||
m.assertEqual(isValid(myList), true)
|
||||
m.assertEqual(isValid(myLongInteger), true)
|
||||
m.assertEqual(isValid(myDouble), true)
|
||||
m.assertEqual(isValid(myFloat), true)
|
||||
m.assertEqual(isValid(myInvalid), false)
|
||||
end function
|
||||
|
||||
@it("works with functions")
|
||||
function _()
|
||||
myfunc = function(a, b)
|
||||
return a + b
|
||||
end function
|
||||
m.assertEqual(isValid(myfunc(0, 1)), true)
|
||||
end function
|
||||
|
||||
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
@describe("isValidAndNotEmpty()")
|
||||
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
@it("works with invalid")
|
||||
@params(invalid, false)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValidAndNotEmpty(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with strings")
|
||||
@params("", false)
|
||||
@params(" ", false)
|
||||
@params("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Augue neque gravida in fermentum et. Eget lorem dolor sed viverra ipsum nunc. At quis risus sed vulputate odio ut enim. Ultricies integer quis auctor elit sed. Egestas congue quisque egestas diam in. Aliquam sem fringilla ut morbi tincidunt. Malesuada bibendum arcu vitae elementum curabitur. Aliquet sagittis id consectetur purus ut faucibus pulvinar. Eget gravida cum sociis natoque. Sollicitudin aliquam ultrices sagittis orci. Ut etiam sit amet nisl purus. Luctus venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Vitae ultricies leo integer malesuada nunc. Vitae ultricies leo integer malesuada nunc vel risus commodo. Luctus accumsan tortor posuere ac ut. Urna cursus eget nunc scelerisque viverra mauris in. Accumsan sit amet nulla facilisi morbi tempus iaculis urna id. Mauris vitae ultricies leo integer malesuada nunc vel risus commodo. Morbi tincidunt augue interdum velit euismod in pellentesque.", true)
|
||||
@params("~!@#$%^&*()_-+=`\|]}';:.,/?", true)
|
||||
@params("true", true)
|
||||
@params("false", true)
|
||||
@params("invalid", true)
|
||||
function _(value, expectedassertResult)
|
||||
m.assertEqual(isValidAndNotEmpty(value), expectedassertResult)
|
||||
end function
|
||||
|
||||
@it("works with arrays")
|
||||
function _()
|
||||
m.assertEqual(isValidAndNotEmpty(m.myEmptyArray), false)
|
||||
m.assertEqual(isValidAndNotEmpty(m.myArray), false)
|
||||
m.myArray.Push("string")
|
||||
m.assertEqual(isValidAndNotEmpty(m.myArray), true)
|
||||
m.myArray.Clear()
|
||||
m.assertEqual(isValidAndNotEmpty(m.myArray), false)
|
||||
end function
|
||||
|
||||
@it("works with associative arrays")
|
||||
function _()
|
||||
m.assertEqual(isValidAndNotEmpty(m.myEmptyArray), false)
|
||||
m.assertEqual(isValidAndNotEmpty(m.myAssArray), true)
|
||||
end function
|
||||
|
||||
@it("works with lists")
|
||||
function _()
|
||||
m.assertEqual(isValidAndNotEmpty(m.myEmptyList), false)
|
||||
m.assertEqual(isValidAndNotEmpty(m.myList), true)
|
||||
end function
|
||||
|
||||
end class
|
||||
end namespace
|
Loading…
Reference in New Issue
Block a user