Compare commits
336 Commits
master
...
feature/mj
Author | SHA1 | Date | |
---|---|---|---|
6158ca2be4 | |||
fd46047f8c | |||
|
0defe7c941 | ||
|
c45e4169c8 | ||
|
8f83e8f3b0 | ||
|
e61353bc96 | ||
|
de27e233a4 | ||
|
0353eef777 | ||
|
4d625e45ab | ||
|
a4f34613f5 | ||
|
5f18ac9d58 | ||
|
b168887bac | ||
|
f08b94302c | ||
|
79f5c15024 | ||
|
064317e7a5 | ||
841b0b08d4 | |||
82126e7523 | |||
4e3b28ea92 | |||
|
acd5142b9c | ||
|
11edc348cf | ||
f929f888ce | |||
|
3499e32dd0 | ||
|
c36198b046 | ||
|
60bd16cff0 | ||
|
a7b170088d | ||
|
d768ea1c32 | ||
|
98d6c551aa | ||
|
e939cd9772 | ||
|
6573aa426d | ||
|
36eabb43cc | ||
|
97a2fee81c | ||
|
cdbe7655d6 | ||
|
5ff768bb07 | ||
|
a4042a1a63 | ||
|
f90fe37e73 | ||
|
00dbb7cbb9 | ||
|
67b5439530 | ||
|
cb9c44867c | ||
|
357c3f1c0d | ||
|
84d5a49762 | ||
|
a6fda12258 | ||
|
ee51fd85d9 | ||
|
5c3b76fc07 | ||
|
fa87a3971a | ||
|
0438e859e5 | ||
|
7f05f8eea6 | ||
|
d8bcdd8bf0 | ||
|
e20425997d | ||
|
a0ff9c3c2d | ||
|
e22e6e9b54 | ||
|
b41ad87466 | ||
|
a189b4e480 | ||
|
2e961aaec7 | ||
|
fde6ddec80 | ||
|
7a8d705ced | ||
|
22c3969eb4 | ||
|
9b21617283 | ||
|
8600cb4df1 | ||
|
3cf821d374 | ||
|
cdd74ee9e4 | ||
|
2868802b1d | ||
|
f0a9f13ca5 | ||
|
f7d99508fb | ||
|
69467f3f8b | ||
|
885a639a58 | ||
|
e6f843fe3d | ||
|
ca30244290 | ||
|
75f1e1e141 | ||
|
1b863ba34c | ||
|
aa9ad629d8 | ||
|
0c59c2a629 | ||
|
39abf06056 | ||
|
5749569423 | ||
|
480e9d9bb5 | ||
|
896efc645d | ||
|
204c977f5c | ||
|
f5f4b436d6 | ||
|
c0c6a63c5a | ||
|
ed5d73de4b | ||
|
ae4ddc6fd9 | ||
|
67d47a1b05 | ||
|
91cddbd595 | ||
|
6a78b14949 | ||
|
d49715a8af | ||
|
11a251454e | ||
|
d9f714690e | ||
|
425512b00f | ||
|
f4f5df2edb | ||
|
7e76f9f802 | ||
|
e8c690b5a2 | ||
|
e488407ba5 | ||
|
558d72c1e0 | ||
|
8d7ccdf0fc | ||
|
e978cdc60b | ||
|
bb96006aa9 | ||
|
c8b4ee656d | ||
|
bfb039ae6b | ||
|
ade479fcae | ||
|
d5718501d4 | ||
|
9431eef87b | ||
|
7b7b7a2a70 | ||
|
cd97aa281f | ||
|
b2ee23ee6b | ||
|
c14dfd28f8 | ||
|
50aa531b7b | ||
|
bee38aea6e | ||
|
4769c155c2 | ||
|
d9ce775ab0 | ||
|
6112fbd760 | ||
|
845ba9f3fd | ||
|
ad7dbbb990 | ||
|
41a57da252 | ||
|
856be7cf7a | ||
|
d75b49763e | ||
|
ed29d4c9fa | ||
|
f354427e08 | ||
|
9c8fab1758 | ||
|
6fd2b97ed6 | ||
|
c44d3bef20 | ||
|
628dab6ce9 | ||
|
244823f470 | ||
|
31f91c7500 | ||
|
ddfb73c5ca | ||
|
d27a0efcc2 | ||
|
1999cf5e0b | ||
|
5e83a96ad1 | ||
|
db8617d541 | ||
|
58c746af49 | ||
|
d5790e150a | ||
|
3fdd8244be | ||
|
ee8de13268 | ||
|
919a53e0ac | ||
|
47c548fab4 | ||
|
d7ca790d4b | ||
|
c83f0f85a4 | ||
|
3258d46576 | ||
|
aa87b440c9 | ||
|
d1143d41b5 | ||
|
7de5e684d3 | ||
|
da3b8e8263 | ||
|
ad44368782 | ||
|
a814b4ef56 | ||
|
9ceef486fa | ||
|
677643809b | ||
|
94355bc551 | ||
|
f786e3a684 | ||
|
f438e1961f | ||
|
a5d49da8b6 | ||
|
3d282f9fdc | ||
|
2a9ecce423 | ||
|
90542aa5eb | ||
|
94e9e95161 | ||
|
1e0be5f918 | ||
|
f285f67d14 | ||
|
3c4763ba21 | ||
|
1246244468 | ||
|
b7e4c4a0e6 | ||
|
5efbc92b05 | ||
|
20d2583daf | ||
|
3d55fba207 | ||
|
74021e7a2d | ||
|
428b8cd73d | ||
|
e102343437 | ||
|
2e3ed14123 | ||
|
ff70036fea | ||
|
f2ac9a49f2 | ||
|
95b974d184 | ||
|
03d3d7815f | ||
|
9ce2e2a9c5 | ||
|
3bfb01f1e1 | ||
|
195081c2d0 | ||
|
0dc3c875f0 | ||
|
73b6b7761d | ||
|
b8a7c1aaae | ||
|
0a4500e600 | ||
|
f7a3618af9 | ||
|
b9634047e8 | ||
|
024df70e12 | ||
|
424951ccdc | ||
|
127d08f1c0 | ||
|
6c9b3f10b5 | ||
|
5db1b48f96 | ||
|
353fdf275e | ||
|
404019c9da | ||
|
a43f42b52b | ||
|
52403ca454 | ||
|
6f88e9a032 | ||
|
a5dbd8fac5 | ||
|
c57e7399ce | ||
|
281b6d37f1 | ||
|
765b36e322 | ||
|
190f34edef | ||
|
e46f1097a3 | ||
|
8aa5da228e | ||
|
b3ee484220 | ||
|
a08f8a646d | ||
|
747de69b0b | ||
|
5be021e662 | ||
|
6edcf29220 | ||
|
3811f298d3 | ||
|
509e2cb031 | ||
|
917109d7ad | ||
|
8bf164ba29 | ||
|
7527d405ed | ||
|
f7f43cbd51 | ||
|
0c733879f1 | ||
|
5aa26f39aa | ||
|
e2be70f90b | ||
|
03941b4d77 | ||
|
5854b787a6 | ||
|
5776339594 | ||
|
985908138e | ||
|
3f172dc3e8 | ||
|
3b888d40a0 | ||
|
d93520bab4 | ||
|
8874b61781 | ||
|
0ddda93887 | ||
|
c4fe6a8c48 | ||
|
2e607465a9 | ||
|
f5675a982a | ||
|
96ac0fb9d2 | ||
|
696e8683dc | ||
|
e38f46ab13 | ||
|
64efebb511 | ||
|
c1f4e9d389 | ||
|
2dbcba8846 | ||
|
d4f4bfc741 | ||
|
7396d53746 | ||
|
f700f53c05 | ||
|
1a875a0a6d | ||
|
95b0158347 | ||
|
7b37ca11db | ||
|
301cd5c58f | ||
|
0d8f9d127b | ||
|
dfcf2e4f95 | ||
|
03b2768d58 | ||
|
5f6880589c | ||
|
39c73755b4 | ||
|
c543e0e373 | ||
|
9e5bdd4979 | ||
|
48c19eda75 | ||
|
526f075851 | ||
|
e5027ab6af | ||
|
ab8c5901a4 | ||
|
2640369753 | ||
|
b5ad638449 | ||
|
8601adfa14 | ||
|
497615e214 | ||
|
7c8719cbf9 | ||
|
286749ce4c | ||
|
3a9987f29c | ||
|
853506d87f | ||
|
29f5d4fa17 | ||
|
cb22a2ab56 | ||
|
945a6d3e33 | ||
|
abafbf53df | ||
|
1c274867fc | ||
|
0947deaa30 | ||
|
769c5f40e8 | ||
|
5baa8f49e7 | ||
|
f35a6d5f16 | ||
|
833ca95d52 | ||
|
d4911dde1b | ||
|
37bc7dcb91 | ||
|
f4a3c12cb8 | ||
|
23b013f771 | ||
|
11f8b60e11 | ||
|
c8d153dc8f | ||
|
f2597f25d5 | ||
|
a37c7d9a7c | ||
|
34c537933b | ||
|
ecf14e4f43 | ||
|
b1f8ee4e69 | ||
|
df6a04f71a | ||
|
0d66a4b778 | ||
|
b9e55e0848 | ||
|
7e3a23467f | ||
|
cdb73e5287 | ||
|
c0c25d2c6f | ||
|
59e649f9ae | ||
|
89fef9d768 | ||
|
19c92f363e | ||
|
a90df38b3a | ||
|
a9e5e92687 | ||
|
df67d34c7f | ||
|
36182ae5eb | ||
|
d6d5d0b979 | ||
|
c480610897 | ||
|
7b5d553f84 | ||
359d3394d3 | |||
|
e6bfafb81d | ||
|
d34b1b2608 | ||
|
b4823691a3 | ||
|
bc1210a2ea | ||
|
0ccfab24da | ||
|
a8466c6e85 | ||
|
2b44960e0a | ||
|
efe61fdd0d | ||
|
cd1a4b4f88 | ||
|
9fe7de08be | ||
|
b46b15c94e | ||
|
a42c9fef40 | ||
|
5c5ab34385 | ||
|
c81ac215e4 | ||
|
48f0afc5dd | ||
|
b6fa9c14f0 | ||
|
b6d67c6305 | ||
|
6fc1f35b5e | ||
|
83c7206a1e | ||
|
6ed0805457 | ||
|
aa9cd2d266 | ||
|
dc46ff4f51 | ||
|
9a598471a0 | ||
|
c9d1fa22d6 | ||
|
0a9744004c | ||
|
20cdb6fe5c | ||
|
51452abe28 | ||
|
b41e08f232 | ||
|
f28268a0d3 | ||
|
5abe8d2c2a | ||
|
b2b67ae923 | ||
|
f74cc10cd4 | ||
|
c6a51633da | ||
|
ff3f01ef9d | ||
|
c1169edb11 | ||
|
680a00cb92 | ||
|
caee4fa5a8 | ||
|
3be939ff36 | ||
|
66b08400b7 | ||
|
fb6fdbe43e | ||
|
326efd0c61 | ||
|
a0e5d65bc6 | ||
|
d9ae77be68 | ||
|
4c66f8a5b3 | ||
|
b55fc6179a | ||
|
9e010f50b2 |
2
.github/workflows/auto-close-stale-pr.yml
vendored
2
.github/workflows/auto-close-stale-pr.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
|
||||
with:
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
|
|
4
.github/workflows/automations.yml
vendored
4
.github/workflows/automations.yml
vendored
|
@ -6,8 +6,6 @@ concurrency:
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- unstable
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
|
@ -16,7 +14,7 @@ jobs:
|
|||
name: Project board 📊
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
- uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
|
8
.github/workflows/build-dev.yml
vendored
8
.github/workflows/build-dev.yml
vendored
|
@ -2,18 +2,14 @@ name: build-dev
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- unstable
|
||||
|
||||
jobs:
|
||||
dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
@ -23,7 +19,7 @@ jobs:
|
|||
run: npm run ropm
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
with:
|
||||
name: Jellyfin-Roku-dev-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/build/staging
|
||||
|
|
2
.github/workflows/build-docs.yml
vendored
2
.github/workflows/build-docs.yml
vendored
|
@ -3,7 +3,7 @@ name: build-docs
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- unstable
|
||||
- master
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
|
|
59
.github/workflows/build-prod.yml
vendored
59
.github/workflows/build-prod.yml
vendored
|
@ -1,68 +1,19 @@
|
|||
# Builds the production version of the app
|
||||
name: build-prod
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "*.*.z"
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master (the latest release)
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Install jq to parse json
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: jq
|
||||
- name: Save old package.json version
|
||||
run: echo "oldPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: Find and save old major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Save old manifest version
|
||||
run: echo "oldManVersion=${{ env.oldMajor }}.${{ env.oldMinor }}.${{ env.oldBuild }}" >> $GITHUB_ENV
|
||||
- name: Save old Makefile version
|
||||
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Save new package.json version
|
||||
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: package.json version must be updated
|
||||
if: env.oldPackVersion == env.newPackVersion
|
||||
run: exit 1
|
||||
- name: Find and save new major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "newMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save new minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "newMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save new build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "newBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Save new manifest version
|
||||
run: echo "newManVersion=${{ env.newMajor }}.${{ env.newMinor }}.${{ env.newBuild }}" >> $GITHUB_ENV
|
||||
- name: Manifest version must be updated
|
||||
if: env.oldManVersion == env.newManVersion
|
||||
run: exit 1
|
||||
- name: Save new Makefile version
|
||||
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Makefile version must be updated
|
||||
if: env.oldMakeVersion == env.newMakeVersion
|
||||
run: exit 1
|
||||
- name: All new versions must match
|
||||
if: (env.newManVersion != env.newPackVersion) || (env.newManVersion != env.newMakeVersion)
|
||||
run: exit 1
|
||||
prod:
|
||||
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'release-prep') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
@ -72,7 +23,7 @@ jobs:
|
|||
run: npm run ropm
|
||||
- name: Build app for production
|
||||
run: npm run build-prod
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
with:
|
||||
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/build/staging
|
||||
|
|
235
.github/workflows/bump-version.yml
vendored
Normal file
235
.github/workflows/bump-version.yml
vendored
Normal file
|
@ -0,0 +1,235 @@
|
|||
name: "Create PR to bump version"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
targetBranch:
|
||||
description: 'Target Branch'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- bugfix
|
||||
- master
|
||||
versionType:
|
||||
description: 'What Version to Bump'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- build
|
||||
- minor
|
||||
- major
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event.inputs.versionType == 'build' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Install required packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: jq
|
||||
- name: Save targetBranch to env
|
||||
if: github.event.inputs.targetBranch != 'bugfix'
|
||||
run: echo "targetBranch=${{ github.event.inputs.targetBranch }}" >> $GITHUB_ENV
|
||||
# Save old version
|
||||
- name: Find and save old major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
# Bugfix branch
|
||||
- name: Save bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "bugfixBranch=${{ env.oldMajor }}.${{ env.oldMinor }}.z" >> $GITHUB_ENV
|
||||
- name: Update targetBranch with actual bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
|
||||
- name: Checkout bugfix branch
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: ${{ env.targetBranch }}
|
||||
# Save old version again if needed
|
||||
- name: Find and save old major_version from manifest
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
# Calculate new version
|
||||
- name: Calculate new build_version
|
||||
run: echo "newBuild=$((${{ env.oldBuild }} + 1))" >> $GITHUB_ENV
|
||||
- name: Save new version to env var
|
||||
run: echo "newVersion=${{ env.oldMajor }}.${{ env.oldMinor }}.${{ env.newBuild }}" >> $GITHUB_ENV
|
||||
- name: Save a copy of newVersion without periods to env var
|
||||
run: echo "newVersionSlug=${{ env.newVersion }}" | sed -e 's/\.//g' >> $GITHUB_ENV
|
||||
# Update files with new versions
|
||||
- name: Update manifest build_version
|
||||
run: sed -i "s/build_version=.*/build_version=${{ env.newBuild }}/g" manifest
|
||||
- name: Update package-lock.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package-lock.json version 2
|
||||
run: echo "$( jq '.packages."".version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package.json )" > package.json
|
||||
- name: Update Makefile version
|
||||
run: sed -i "s/VERSION := .*/VERSION := ${{ env.newVersion }}/g" Makefile
|
||||
# Create PR
|
||||
- name: Save new branch name to env
|
||||
run: echo "newBranch=bump-${{ github.event.inputs.targetBranch }}-to-${{ env.newVersionSlug }}" >> $GITHUB_ENV
|
||||
- name: Create PR with new version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
run: |-
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
git checkout -b "${{ env.newBranch }}"
|
||||
git add .
|
||||
git commit -m "Bump ${{ github.event.inputs.versionType }} version"
|
||||
git push --set-upstream origin "${{ env.newBranch }}"
|
||||
gh pr create --title "Bump ${{ github.event.inputs.targetBranch }} branch to ${{ env.newVersion }}" --body "Bump version to prep for next release." --label ignore-changelog --base ${{ env.targetBranch }}
|
||||
minor:
|
||||
if: ${{ github.event.inputs.versionType == 'minor' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Install jq to update json
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: jq
|
||||
- name: Save targetBranch to env
|
||||
if: github.event.inputs.targetBranch != 'bugfix'
|
||||
run: echo "targetBranch=${{ github.event.inputs.targetBranch }}" >> $GITHUB_ENV
|
||||
# Save old version
|
||||
- name: Find and save old major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
# Bugfix branch
|
||||
- name: Save bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "bugfixBranch=${{ env.oldMajor }}.${{ env.oldMinor }}.z" >> $GITHUB_ENV
|
||||
- name: Update targetBranch with actual bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
|
||||
- name: Checkout bugfix branch
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: ${{ env.targetBranch }}
|
||||
# Calculate new version
|
||||
- name: Calculate new build_version
|
||||
run: echo "newMinor=$((${{ env.oldMinor }} + 1))" >> $GITHUB_ENV
|
||||
- name: Save new version to env var
|
||||
run: echo "newVersion=${{ env.oldMajor }}.${{ env.newMinor }}.0" >> $GITHUB_ENV
|
||||
- name: Save a copy of newVersion without periods to env var
|
||||
run: echo "newVersionSlug=${{ env.newVersion }}" | sed -e 's/\.//g' >> $GITHUB_ENV
|
||||
# Update files with new versions
|
||||
- name: Update manifest minor_version
|
||||
run: sed -i "s/minor_version=.*/minor_version=${{ env.newMinor }}/g" manifest
|
||||
- name: Update manifest build_version
|
||||
run: sed -i "s/build_version=.*/build_version=0/g" manifest
|
||||
- name: Update package-lock.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package-lock.json version 2
|
||||
run: echo "$( jq '.packages."".version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package.json )" > package.json
|
||||
- name: Update Makefile version
|
||||
run: sed -i "s/VERSION := .*/VERSION := ${{ env.newVersion }}/g" Makefile
|
||||
# Create PR
|
||||
- name: Save new branch name to env
|
||||
run: echo "newBranch=bump-${{ github.event.inputs.targetBranch }}-to-${{ env.newVersionSlug }}" >> $GITHUB_ENV
|
||||
- name: Create PR with new version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
run: |-
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
git checkout -b "${{ env.newBranch }}"
|
||||
git add .
|
||||
git commit -m "Bump ${{ github.event.inputs.versionType }} version"
|
||||
git push --set-upstream origin "${{ env.newBranch }}"
|
||||
gh pr create --title "Bump ${{ github.event.inputs.targetBranch }} branch to ${{ env.newVersion }}" --body "Bump version to prep for next release." --label ignore-changelog --base ${{ env.targetBranch }}
|
||||
|
||||
major:
|
||||
if: ${{ github.event.inputs.versionType == 'major' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Install jq to update json
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: jq
|
||||
- name: Save targetBranch to env
|
||||
if: github.event.inputs.targetBranch != 'bugfix'
|
||||
run: echo "targetBranch=${{ github.event.inputs.targetBranch }}" >> $GITHUB_ENV
|
||||
# Save old version
|
||||
- name: Find and save old major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
# Bugfix branch
|
||||
- name: Save bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "bugfixBranch=${{ env.oldMajor }}.${{ env.oldMinor }}.z" >> $GITHUB_ENV
|
||||
- name: Update targetBranch with actual bugfix branch name
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
|
||||
- name: Checkout bugfix branch
|
||||
if: github.event.inputs.targetBranch == 'bugfix'
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: ${{ env.targetBranch }}
|
||||
# Calculate new version
|
||||
- name: Calculate new build_version
|
||||
run: echo "newMajor=$((${{ env.oldMajor }} + 1))" >> $GITHUB_ENV
|
||||
- name: Save new version to env var
|
||||
run: echo "newVersion=${{ env.newMajor }}.0.0" >> $GITHUB_ENV
|
||||
- name: Save a copy of newVersion without periods to env var
|
||||
run: echo "newVersionSlug=${{ env.newVersion }}" | sed -e 's/\.//g' >> $GITHUB_ENV
|
||||
# Update files with new versions
|
||||
- name: Update manifest major_version
|
||||
run: sed -i "s/major_version=.*/major_version=${{ env.newMajor }}/g" manifest
|
||||
- name: Update manifest minor_version
|
||||
run: sed -i "s/minor_version=.*/minor_version=0/g" manifest
|
||||
- name: Update manifest build_version
|
||||
run: sed -i "s/build_version=.*/build_version=0/g" manifest
|
||||
- name: Update package-lock.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package-lock.json version 2
|
||||
run: echo "$( jq '.packages."".version = "'"${{ env.newVersion }}"'"' package-lock.json )" > package-lock.json
|
||||
- name: Update package.json version
|
||||
run: echo "$( jq '.version = "'"${{ env.newVersion }}"'"' package.json )" > package.json
|
||||
- name: Update Makefile version
|
||||
run: sed -i "s/VERSION := .*/VERSION := ${{ env.newVersion }}/g" Makefile
|
||||
# Create PR
|
||||
- name: Save new branch name to env
|
||||
run: echo "newBranch=bump-${{ github.event.inputs.targetBranch }}-to-${{ env.newVersionSlug }}" >> $GITHUB_ENV
|
||||
- name: Create PR with new version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
run: |-
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
git checkout -b "${{ env.newBranch }}"
|
||||
git add .
|
||||
git commit -m "Bump ${{ github.event.inputs.versionType }} version"
|
||||
git push --set-upstream origin "${{ env.newBranch }}"
|
||||
gh pr create --title "Bump ${{ github.event.inputs.targetBranch }} branch to ${{ env.newVersion }}" --body "Bump version to prep for next release." --label ignore-changelog --base ${{ env.targetBranch }}
|
||||
|
6
.github/workflows/deploy-api-docs.yml
vendored
6
.github/workflows/deploy-api-docs.yml
vendored
|
@ -3,7 +3,7 @@ name: deploy-api-docs
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: ["unstable"]
|
||||
branches: ["master"]
|
||||
paths: ["docs/**"] # only run if the docs are updated
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
|
@ -34,10 +34,10 @@ jobs:
|
|||
- name: Setup Pages
|
||||
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
with:
|
||||
# Only upload the api docs folder
|
||||
path: "docs/api"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@77d7344265e1f960dab5c00dbff52287a70b0d4f # v3
|
||||
uses: actions/deploy-pages@decdde0ac072f6dcbe43649d82d9c635fff5b4e4 # v4
|
||||
|
|
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
|
@ -2,7 +2,6 @@ name: lint
|
|||
on:
|
||||
pull_request:
|
||||
|
||||
|
||||
jobs:
|
||||
brightscript:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -62,7 +61,7 @@ jobs:
|
|||
run: npm ci
|
||||
- name: Install roku package dependencies
|
||||
run: npx ropm install
|
||||
- uses: xt0rted/markdownlint-problem-matcher@98d94724052d20ca2e06c091f202e4c66c3c59fb # v2
|
||||
- uses: xt0rted/markdownlint-problem-matcher@1a5fabfb577370cfdf5af944d418e4be3ea06f27 # v3
|
||||
- name: Lint markdown files
|
||||
run: npm run lint-markdown
|
||||
spelling:
|
||||
|
|
80
.github/workflows/release-prep.yml
vendored
Normal file
80
.github/workflows/release-prep.yml
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
# All of the jobs in this workflow will only run if the PR that triggered it has a 'release-prep' label
|
||||
name: release-prep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'release-prep') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: DEBUG ${{ github.event.pull_request.base.ref }}
|
||||
run: echo ${{ github.event.pull_request.base.ref }}
|
||||
- name: Checkout the branch this PR wants to update
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
- name: Install jq to parse json
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: jq
|
||||
- name: Save old package.json version
|
||||
run: echo "oldPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: Find and save old major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save old build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Save old manifest version
|
||||
run: echo "oldManVersion=${{ env.oldMajor }}.${{ env.oldMinor }}.${{ env.oldBuild }}" >> $GITHUB_ENV
|
||||
- name: Save old Makefile version
|
||||
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Save new package.json version
|
||||
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||
- name: package.json version must be updated
|
||||
if: env.oldPackVersion == env.newPackVersion
|
||||
run: exit 1
|
||||
- name: Find and save new major_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^major_version/ { print "newMajor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save new minor_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "newMinor="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Find and save new build_version from manifest
|
||||
run: awk 'BEGIN { FS="=" } /^build_version/ { print "newBuild="$2; }' manifest >> $GITHUB_ENV
|
||||
- name: Save new manifest version
|
||||
run: echo "newManVersion=${{ env.newMajor }}.${{ env.newMinor }}.${{ env.newBuild }}" >> $GITHUB_ENV
|
||||
- name: Manifest version must be updated
|
||||
if: env.oldManVersion == env.newManVersion
|
||||
run: exit 1
|
||||
- name: Save new Makefile version
|
||||
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||
- name: Makefile version must be updated
|
||||
if: env.oldMakeVersion == env.newMakeVersion
|
||||
run: exit 1
|
||||
- name: All new versions must match
|
||||
if: (env.newManVersion != env.newPackVersion) || (env.newManVersion != env.newMakeVersion)
|
||||
run: exit 1
|
||||
build-prod:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'release-prep') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
- name: NPM install
|
||||
run: npm ci
|
||||
- name: Install roku module dependencies
|
||||
run: npm run ropm
|
||||
- name: Build app for production
|
||||
run: npm run build-prod
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
with:
|
||||
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
|
||||
path: ${{ github.workspace }}/build/staging
|
||||
if-no-files-found: error
|
4
.github/workflows/roku-analysis.yml
vendored
4
.github/workflows/roku-analysis.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
@ -27,7 +27,7 @@ jobs:
|
|||
if: env.BRANCH_NAME == 'master'
|
||||
run: npm run build-prod
|
||||
- name: Use Java 17
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
|
2
Makefile
2
Makefile
|
@ -3,7 +3,7 @@
|
|||
# If you want to get_images, you'll also need convert from ImageMagick
|
||||
##########################################################################
|
||||
|
||||
VERSION := 2.0.0
|
||||
VERSION := 2.0.5
|
||||
|
||||
## usage
|
||||
|
||||
|
|
|
@ -31,9 +31,9 @@
|
|||
],
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"autoImportComponentScript": true,
|
||||
"allowBrighterScriptInBrightScript": true,
|
||||
"createPackage": false,
|
||||
"stagingFolderPath": "build",
|
||||
"retainStagingDir": true,
|
||||
"plugins": ["rooibos-roku"],
|
||||
"rooibos": {
|
||||
"isRecordingCodeCoverage": false,
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
{
|
||||
"src": "settings/**/*",
|
||||
"dest": "settings"
|
||||
}
|
||||
},
|
||||
"manifest"
|
||||
],
|
||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||
"autoImportComponentScript": true,
|
||||
"allowBrighterScriptInBrightScript": true,
|
||||
"stagingFolderPath": "build",
|
||||
"retainStagingDir": true,
|
||||
"plugins": ["rooibos-roku"],
|
||||
"rooibos": {
|
||||
"isRecordingCodeCoverage": false,
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
' @fileoverview Clock component to display current time formatted based on user's chosen 12 or 24 hour setting
|
||||
|
||||
' Possible clock formats
|
||||
enum ClockFormat
|
||||
h12 = "12h"
|
||||
h24 = "24h"
|
||||
end enum
|
||||
|
||||
sub init()
|
||||
|
||||
' If hideclick setting is checked, exit without setting any variables
|
||||
' If hideclick setting is enabled, exit without setting any variables
|
||||
if m.global.session.user.settings["ui.design.hideclock"]
|
||||
return
|
||||
end if
|
||||
|
@ -16,11 +24,11 @@ sub init()
|
|||
m.currentTimeTimer.control = "start"
|
||||
|
||||
' Default to 12 hour clock
|
||||
m.format = "short-h12"
|
||||
m.format = ClockFormat.h12
|
||||
|
||||
' If user has selected a 24 hour clock, update date display format
|
||||
if LCase(m.global.device.clockFormat) = "24h"
|
||||
m.format = "short-h24"
|
||||
if LCase(m.global.device.clockFormat) = ClockFormat.h24
|
||||
m.format = ClockFormat.h24
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -34,6 +42,64 @@ sub onCurrentTimeTimerFire()
|
|||
' Convert to local time zone
|
||||
m.dateTimeObject.ToLocalTime()
|
||||
|
||||
' Format time as requested
|
||||
m.clockTime.text = m.dateTimeObject.asTimeStringLoc(m.format)
|
||||
' Format time for display - based on 12h/24h setting
|
||||
formattedTime = formatTimeAsString()
|
||||
|
||||
' Display time
|
||||
m.clockTime.text = formattedTime
|
||||
end sub
|
||||
|
||||
' formatTimeAsString: Returns a string with the current time formatted for either a 12 or 24 hour clock
|
||||
'
|
||||
' @return {string} current time formatted for either a 12 hour or 24 hour clock
|
||||
function formatTimeAsString() as string
|
||||
return m.format = ClockFormat.h12 ? format12HourTime() : format24HourTime()
|
||||
end function
|
||||
|
||||
' format12HourTime: Returns a string with the current time formatted for a 12 hour clock
|
||||
'
|
||||
' @return {string} current time formatted for a 12 hour clock
|
||||
function format12HourTime() as string
|
||||
currentHour = m.dateTimeObject.GetHours()
|
||||
currentMinute = m.dateTimeObject.GetMinutes()
|
||||
|
||||
displayedHour = StrI(currentHour).trim()
|
||||
displayedMinute = StrI(currentMinute).trim()
|
||||
meridian = currentHour < 12 ? "am" : "pm"
|
||||
|
||||
if currentHour = 0
|
||||
displayedHour = "12"
|
||||
end if
|
||||
|
||||
if currentHour > 12
|
||||
correctedHour = currentHour - 12
|
||||
displayedHour = StrI(correctedHour).trim()
|
||||
end if
|
||||
|
||||
if currentMinute < 10
|
||||
displayedMinute = `0${displayedMinute}`
|
||||
end if
|
||||
|
||||
return `${displayedHour}:${displayedMinute} ${meridian}`
|
||||
end function
|
||||
|
||||
' format24HourTime: Returns a string with the current time formatted for a 24 hour clock
|
||||
'
|
||||
' @return {string} current time formatted for a 24 hour clock
|
||||
function format24HourTime() as string
|
||||
currentHour = m.dateTimeObject.GetHours()
|
||||
currentMinute = m.dateTimeObject.GetMinutes()
|
||||
|
||||
displayedHour = StrI(currentHour).trim()
|
||||
displayedMinute = StrI(currentMinute).trim()
|
||||
|
||||
if currentHour < 10
|
||||
displayedHour = `0${displayedHour}`
|
||||
end if
|
||||
|
||||
if currentMinute < 10
|
||||
displayedMinute = `0${displayedMinute}`
|
||||
end if
|
||||
|
||||
return `${displayedHour}:${displayedMinute}`
|
||||
end function
|
||||
|
|
|
@ -48,7 +48,6 @@
|
|||
</children>
|
||||
<interface>
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
<field id="focusedChild" type="node" onChange="focusChanged" />
|
||||
<field id="itemAlphaSelected" type="string" />
|
||||
</interface>
|
||||
</component>
|
|
@ -15,6 +15,10 @@ sub init()
|
|||
|
||||
m.unplayedCount = m.top.findNode("unplayedCount")
|
||||
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
|
||||
m.playedIndicator = m.top.findNode("playedIndicator")
|
||||
m.checkmark = m.top.findNode("checkmark")
|
||||
m.checkmark.width = 90
|
||||
m.checkmark.height = 60
|
||||
|
||||
m.itemText.translation = [0, m.itemPoster.height + 7]
|
||||
|
||||
|
@ -34,31 +38,43 @@ sub init()
|
|||
end sub
|
||||
|
||||
sub itemContentChanged()
|
||||
m.backdrop.blendColor = "#00a4db" ' set default in case global var is invalid
|
||||
localGlobal = m.global
|
||||
|
||||
' Set Random background colors from pallet
|
||||
posterBackgrounds = m.global.constants.poster_bg_pallet
|
||||
m.backdrop.blendColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
if isValid(localGlobal) and isValid(localGlobal.constants) and isValid(localGlobal.constants.poster_bg_pallet)
|
||||
posterBackgrounds = localGlobal.constants.poster_bg_pallet
|
||||
m.backdrop.blendColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
end if
|
||||
|
||||
itemData = m.top.itemContent
|
||||
|
||||
if itemData = invalid then return
|
||||
|
||||
if itemData.type = "Movie"
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
|
||||
m.playedIndicator.visible = true
|
||||
end if
|
||||
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
m.itemText.text = itemData.Title
|
||||
else if itemData.type = "Series"
|
||||
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
else
|
||||
m.unplayedCount.visible = false
|
||||
m.unplayedEpisodeCount.text = ""
|
||||
if isValid(localGlobal) and isValid(localGlobal.session) and isValid(localGlobal.session.user) and isValid(localGlobal.session.user.settings)
|
||||
if localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
else
|
||||
m.unplayedCount.visible = false
|
||||
m.unplayedEpisodeCount.text = ""
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played = true
|
||||
m.playedIndicator.visible = true
|
||||
end if
|
||||
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" opacity=".99" translation="[201, 0]">
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="90" height="60" opacity=".99" translation="[201, 0]" visible="false" />
|
||||
</Poster>
|
||||
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
|
||||
<Label id="posterText" width="280" height="415" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
|
||||
|
|
|
@ -8,6 +8,7 @@ sub init()
|
|||
m.posterText.font.size = 30
|
||||
m.title.font.size = 25
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
m.playedIndicator = m.top.findNode("playedIndicator")
|
||||
|
||||
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
|
||||
|
||||
|
@ -37,6 +38,10 @@ sub itemContentChanged()
|
|||
|
||||
if not isValid(itemData) then return
|
||||
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
|
||||
m.playedIndicator.visible = true
|
||||
end if
|
||||
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.posterText.text = itemData.title
|
||||
m.title.text = itemData.title
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<ScrollingLabel translation="[0,340]" id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" maxWidth="230" />
|
||||
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
|
||||
<Label id="posterText" width="230" height="320" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
|
||||
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="60" height="46" translation="[170, 15]" visible="false" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
||||
|
|
|
@ -256,7 +256,8 @@ sub setMoviesOptions(options)
|
|||
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
|
||||
{ "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("RUNTIME"), "Name": "Runtime" }
|
||||
{ "Title": tr("RUNTIME"), "Name": "Runtime" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
options.filter = [
|
||||
{ "Title": tr("All"), "Name": "All" },
|
||||
|
@ -275,6 +276,7 @@ sub setBoxsetsOptions(options)
|
|||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
|
||||
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
options.filter = [
|
||||
{ "Title": tr("All"), "Name": "All" },
|
||||
|
@ -299,6 +301,7 @@ sub setTvShowsOptions(options)
|
|||
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
|
||||
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
options.filter = [
|
||||
{ "Title": tr("All"), "Name": "All" },
|
||||
|
@ -345,6 +348,7 @@ sub setMusicOptions(options)
|
|||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
|
||||
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
options.filter = [
|
||||
{ "Title": tr("All"), "Name": "All" },
|
||||
|
|
|
@ -136,6 +136,36 @@ sub loadItems()
|
|||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
|
||||
' If user has filtered by #, include special characters sorted after Z as well
|
||||
if isValid(params.NameLessThan)
|
||||
if LCase(params.NameLessThan) = "a"
|
||||
' Use same params except for name filter param
|
||||
params.NameLessThan = ""
|
||||
params.NameStartsWithOrGreater = "z"
|
||||
|
||||
' Perform 2nd API lookup for items starting with Z or greater
|
||||
startsWithZAndGreaterResp = APIRequest(url, params)
|
||||
startsWithZAndGreaterData = getJson(startsWithZAndGreaterResp)
|
||||
|
||||
if isValidAndNotEmpty(startsWithZAndGreaterData)
|
||||
specialCharacterItems = []
|
||||
|
||||
' Filter out items starting with Z
|
||||
for each item in startsWithZAndGreaterData.Items
|
||||
itemName = LCase(item.name)
|
||||
if not itemName.StartsWith("z")
|
||||
specialCharacterItems.Push(item)
|
||||
end if
|
||||
end for
|
||||
|
||||
' Append data to results from before A
|
||||
data.Items.Append(specialCharacterItems)
|
||||
data.TotalRecordCount += specialCharacterItems.Count()
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
if data <> invalid
|
||||
|
||||
if data.TotalRecordCount <> invalid then m.top.totalRecordCount = data.TotalRecordCount
|
||||
|
@ -164,6 +194,8 @@ sub loadItems()
|
|||
tmp.image = PosterImage(item.id, { "maxHeight": 425, "maxWidth": 290, "quality": "90" })
|
||||
else if item.type = "Episode"
|
||||
tmp = CreateObject("roSGNode", "TVEpisode")
|
||||
else if LCase(item.Type) = "recording"
|
||||
tmp = CreateObject("roSGNode", "RecordingData")
|
||||
else if item.Type = "Genre"
|
||||
tmp = CreateObject("roSGNode", "ContentNode")
|
||||
tmp.title = item.name
|
||||
|
|
|
@ -7,6 +7,11 @@ import "pkg:/source/api/Image.bs"
|
|||
import "pkg:/source/api/userauth.bs"
|
||||
import "pkg:/source/utils/deviceCapabilities.bs"
|
||||
|
||||
enum SubtitleSelection
|
||||
notset = -2
|
||||
none = -1
|
||||
end enum
|
||||
|
||||
sub init()
|
||||
m.user = AboutMe()
|
||||
m.top.functionName = "loadItems"
|
||||
|
@ -44,19 +49,18 @@ sub loadItems()
|
|||
id = m.top.itemId
|
||||
mediaSourceId = invalid
|
||||
audio_stream_idx = m.top.selectedAudioStreamIndex
|
||||
subtitle_idx = m.top.selectedSubtitleIndex
|
||||
forceTranscoding = false
|
||||
|
||||
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)]
|
||||
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, forceTranscoding)]
|
||||
end sub
|
||||
|
||||
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) as dynamic
|
||||
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) as dynamic
|
||||
|
||||
video = {}
|
||||
video.id = id
|
||||
video.content = createObject("RoSGNode", "ContentNode")
|
||||
|
||||
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)
|
||||
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, forceTranscoding)
|
||||
|
||||
if video.content = invalid
|
||||
return invalid
|
||||
|
@ -65,9 +69,10 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic,
|
|||
return video
|
||||
end function
|
||||
|
||||
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean)
|
||||
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean)
|
||||
|
||||
meta = ItemMetaData(video.id)
|
||||
subtitle_idx = m.top.selectedSubtitleIndex
|
||||
|
||||
if not isValid(meta)
|
||||
video.errorMsg = "Error loading metadata"
|
||||
|
@ -76,16 +81,58 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||
end if
|
||||
|
||||
videotype = LCase(meta.type)
|
||||
|
||||
' Check for any Live TV streams or Recordings coming from other places other than the TV Guide
|
||||
if videotype = "recording" or (isValid(meta.json) and isValid(meta.json.ChannelId))
|
||||
if isValid(meta.json.EpisodeTitle)
|
||||
meta.title = meta.json.EpisodeTitle
|
||||
else if isValid(meta.json.Name)
|
||||
meta.title = meta.json.Name
|
||||
end if
|
||||
meta.live = true
|
||||
if LCase(meta.json.type) = "program"
|
||||
video.id = meta.json.ChannelId
|
||||
else
|
||||
video.id = meta.json.id
|
||||
end if
|
||||
end if
|
||||
|
||||
if videotype = "episode" or videotype = "series"
|
||||
video.content.contenttype = "episode"
|
||||
video.content.title = "S" + meta.json.ParentIndexNumber.toStr() + "E" + meta.json.IndexNumber.toStr() + " - " + meta.title
|
||||
if Type(meta.json.ParentIndexNumber) = "roInt" and Type(meta.json.IndexNumber) = "roInt" and Type(meta.json.ProductionYear) = "roInt"
|
||||
video.content.title = "S" + meta.json.ParentIndexNumber.toStr() + " - E" + meta.json.IndexNumber.toStr() + " - " + meta.title + + " (" + meta.json.ProductionYear.ToStr() + ")"
|
||||
else
|
||||
video.content.title = meta.title
|
||||
end if
|
||||
else
|
||||
video.content.title = meta.title + " (" + meta.json.ProductionYear.ToStr() + ")"
|
||||
if Type(meta.json.ProductionYear) = "roInt"
|
||||
video.content.title = meta.title + " (" + meta.json.ProductionYear.ToStr() + ")"
|
||||
else
|
||||
video.content.title = meta.title
|
||||
end if
|
||||
end if
|
||||
|
||||
video.chapters = meta.json.Chapters
|
||||
video.showID = meta.showID
|
||||
|
||||
logoLookupID = video.id
|
||||
|
||||
if videotype = "episode" or videotype = "series"
|
||||
video.content.contenttype = "episode"
|
||||
video.seasonNumber = meta.json.ParentIndexNumber
|
||||
video.episodeNumber = meta.json.IndexNumber
|
||||
video.episodeNumberEnd = meta.json.IndexNumberEnd
|
||||
|
||||
if isValid(meta.showID)
|
||||
logoLookupID = meta.showID
|
||||
end if
|
||||
end if
|
||||
|
||||
logoImageExists = api.items.HeadImageURLByName(logoLookupID, "logo")
|
||||
if logoImageExists
|
||||
video.logoImage = api.items.GetImageURL(logoLookupID, "logo", 0, { "maxHeight": 65, "maxWidth": 300, "quality": "90" })
|
||||
end if
|
||||
|
||||
user = AboutMe()
|
||||
if user.Configuration.EnableNextEpisodeAutoPlay
|
||||
if LCase(m.top.itemType) = "episode"
|
||||
|
@ -108,16 +155,41 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||
if meta.live then mediaSourceId = ""
|
||||
|
||||
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
|
||||
video.videoId = video.id
|
||||
video.mediaSourceId = mediaSourceId
|
||||
video.audioIndex = audio_stream_idx
|
||||
|
||||
if not isValid(m.playbackInfo)
|
||||
video.errorMsg = "Error loading playback info"
|
||||
video.content = invalid
|
||||
return
|
||||
end if
|
||||
|
||||
addSubtitlesToVideo(video, meta)
|
||||
|
||||
' Enable default subtitle track
|
||||
if subtitle_idx = SubtitleSelection.notset
|
||||
defaultSubtitleIndex = defaultSubtitleTrackFromVid(video.id)
|
||||
|
||||
if defaultSubtitleIndex <> SubtitleSelection.none
|
||||
video.SelectedSubtitle = defaultSubtitleIndex
|
||||
subtitle_idx = defaultSubtitleIndex
|
||||
|
||||
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
|
||||
if not isValid(m.playbackInfo)
|
||||
video.errorMsg = "Error loading playback info"
|
||||
video.content = invalid
|
||||
return
|
||||
end if
|
||||
|
||||
addSubtitlesToVideo(video, meta)
|
||||
else
|
||||
video.SelectedSubtitle = subtitle_idx
|
||||
end if
|
||||
else
|
||||
video.SelectedSubtitle = subtitle_idx
|
||||
end if
|
||||
|
||||
video.videoId = video.id
|
||||
video.mediaSourceId = mediaSourceId
|
||||
video.audioIndex = audio_stream_idx
|
||||
|
||||
video.PlaySessionId = m.playbackInfo.PlaySessionId
|
||||
|
||||
if meta.live
|
||||
|
@ -131,8 +203,7 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||
m.playbackInfo = meta.json
|
||||
end if
|
||||
|
||||
addSubtitlesToVideo(video, meta)
|
||||
|
||||
addAudioStreamsToVideo(video)
|
||||
if meta.live
|
||||
video.transcodeParams = {
|
||||
"MediaSourceId": m.playbackInfo.MediaSources[0].Id,
|
||||
|
@ -184,13 +255,113 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
|||
setCertificateAuthority(video.content)
|
||||
video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based
|
||||
|
||||
video.SelectedSubtitle = subtitle_idx
|
||||
|
||||
if not fully_external
|
||||
video.content = authRequest(video.content)
|
||||
end if
|
||||
end sub
|
||||
|
||||
' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id
|
||||
'
|
||||
' @param {dynamic} videoID - id of video user is playing
|
||||
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
|
||||
function defaultSubtitleTrackFromVid(videoID) as integer
|
||||
if m.global.session.user.configuration.SubtitleMode = "None"
|
||||
return SubtitleSelection.none ' No subtitles desired: return none
|
||||
end if
|
||||
|
||||
meta = ItemMetaData(videoID)
|
||||
|
||||
if not isValid(meta) then return SubtitleSelection.none
|
||||
if not isValid(meta.json) then return SubtitleSelection.none
|
||||
if not isValidAndNotEmpty(meta.json.mediaSources) then return SubtitleSelection.none
|
||||
if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none
|
||||
|
||||
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
|
||||
|
||||
selectedAudioLanguage = ""
|
||||
audioMediaStream = meta.json.MediaSources[0].MediaStreams[m.top.selectedAudioStreamIndex]
|
||||
|
||||
' Ensure audio media stream is valid before using language property
|
||||
if isValid(audioMediaStream)
|
||||
selectedAudioLanguage = audioMediaStream.Language ?? ""
|
||||
end if
|
||||
|
||||
defaultTextSubs = defaultSubtitleTrack(subtitles["text"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text)
|
||||
if defaultTextSubs <> SubtitleSelection.none
|
||||
return defaultTextSubs
|
||||
end if
|
||||
|
||||
if not m.global.session.user.settings["playback.subs.onlytext"]
|
||||
return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text
|
||||
end if
|
||||
|
||||
return SubtitleSelection.none
|
||||
end function
|
||||
|
||||
' defaultSubtitleTrack:
|
||||
'
|
||||
' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language
|
||||
' @param {string} selectedAudioLanguage - language for selected audio track
|
||||
' @param {boolean} [requireText=false] - indicates if only text subtitles should be considered
|
||||
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
|
||||
function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer
|
||||
userConfig = m.global.session.user.configuration
|
||||
|
||||
subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : ""
|
||||
|
||||
allowSmartMode = false
|
||||
|
||||
' Only evaluate selected audio language if we have a value
|
||||
if selectedAudioLanguage <> ""
|
||||
allowSmartMode = selectedAudioLanguage <> userConfig.SubtitleLanguagePreference
|
||||
end if
|
||||
|
||||
for each item in sortedSubtitles
|
||||
' Only auto-select subtitle if language matches SubtitleLanguagePreference
|
||||
languageMatch = true
|
||||
if userConfig.SubtitleLanguagePreference <> ""
|
||||
languageMatch = (userConfig.SubtitleLanguagePreference = item.Track.Language)
|
||||
end if
|
||||
|
||||
' Ensure textuality of subtitle matches preferenced passed as arg
|
||||
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
|
||||
|
||||
if languageMatch and matchTextReq
|
||||
if subtitleMode = "default" and (item.isForced or item.IsDefault)
|
||||
' Return first forced or default subtitle track
|
||||
return item.Index
|
||||
else if subtitleMode = "always"
|
||||
' Return the first found subtitle track
|
||||
return item.Index
|
||||
else if subtitleMode = "onlyforced" and item.IsForced
|
||||
' Return first forced subtitle track
|
||||
return item.Index
|
||||
else if subtitleMode = "smart" and allowSmartMode
|
||||
' Return the first found subtitle track
|
||||
return item.Index
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
|
||||
' User has chosed smart subtitle mode
|
||||
' We already attempted to load subtitles in preferred language, but none were found.
|
||||
' Fall back to default behaviour while ignoring preferredlanguage
|
||||
if subtitleMode = "smart" and allowSmartMode
|
||||
for each item in sortedSubtitles
|
||||
' Ensure textuality of subtitle matches preferenced passed as arg
|
||||
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
|
||||
if matchTextReq
|
||||
if item.isForced or item.IsDefault
|
||||
' Return first forced or default subtitle track
|
||||
return item.Index
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
return SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified
|
||||
end function
|
||||
|
||||
sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
||||
if protocol <> "file"
|
||||
|
@ -220,6 +391,23 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|||
end if
|
||||
end sub
|
||||
|
||||
|
||||
' addAudioStreamsToVideo: Add audio stream data to video
|
||||
'
|
||||
' @param {dynamic} video component to add fullAudioData to
|
||||
sub addAudioStreamsToVideo(video)
|
||||
audioStreams = []
|
||||
mediaStreams = m.playbackInfo.MediaSources[0].MediaStreams
|
||||
|
||||
for i = 0 to mediaStreams.Count() - 1
|
||||
if LCase(mediaStreams[i].Type) = "audio"
|
||||
audioStreams.push(mediaStreams[i])
|
||||
end if
|
||||
end for
|
||||
|
||||
video.fullAudioData = audioStreams
|
||||
end sub
|
||||
|
||||
sub addSubtitlesToVideo(video, meta)
|
||||
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
|
||||
safesubs = subtitles["all"]
|
||||
|
@ -359,26 +547,33 @@ function sortSubtitles(id as string, MediaStreams)
|
|||
"IsExternal": stream.IsExternal,
|
||||
"IsEncoded": stream.DeliveryMethod = "Encode"
|
||||
}
|
||||
|
||||
if stream.isForced
|
||||
trackType = "forced"
|
||||
else if stream.IsDefault
|
||||
trackType = "default"
|
||||
else if stream.IsTextSubtitleStream
|
||||
trackType = "text"
|
||||
else
|
||||
trackType = "normal"
|
||||
end if
|
||||
|
||||
if prefered_lang <> "" and prefered_lang = stream.Track.Language
|
||||
tracks[trackType].unshift(stream)
|
||||
|
||||
if stream.IsTextSubtitleStream
|
||||
tracks["text"].unshift(stream)
|
||||
end if
|
||||
else
|
||||
tracks[trackType].push(stream)
|
||||
|
||||
if stream.IsTextSubtitleStream
|
||||
tracks["text"].push(stream)
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
|
||||
tracks["default"].append(tracks["normal"])
|
||||
tracks["forced"].append(tracks["default"])
|
||||
tracks["forced"].append(tracks["text"])
|
||||
|
||||
return { "all": tracks["forced"], "text": tracks["text"] }
|
||||
end function
|
||||
|
@ -414,497 +609,3 @@ function FindPreferredAudioStream(streams as dynamic) as integer
|
|||
|
||||
return 1
|
||||
end function
|
||||
|
||||
function getSubtitleLanguages()
|
||||
return {
|
||||
"aar": "Afar",
|
||||
"abk": "Abkhazian",
|
||||
"ace": "Achinese",
|
||||
"ach": "Acoli",
|
||||
"ada": "Adangme",
|
||||
"ady": "Adyghe; Adygei",
|
||||
"afa": "Afro-Asiatic languages",
|
||||
"afh": "Afrihili",
|
||||
"afr": "Afrikaans",
|
||||
"ain": "Ainu",
|
||||
"aka": "Akan",
|
||||
"akk": "Akkadian",
|
||||
"alb": "Albanian",
|
||||
"ale": "Aleut",
|
||||
"alg": "Algonquian languages",
|
||||
"alt": "Southern Altai",
|
||||
"amh": "Amharic",
|
||||
"ang": "English, Old (ca.450-1100)",
|
||||
"anp": "Angika",
|
||||
"apa": "Apache languages",
|
||||
"ara": "Arabic",
|
||||
"arc": "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)",
|
||||
"arg": "Aragonese",
|
||||
"arm": "Armenian",
|
||||
"arn": "Mapudungun; Mapuche",
|
||||
"arp": "Arapaho",
|
||||
"art": "Artificial languages",
|
||||
"arw": "Arawak",
|
||||
"asm": "Assamese",
|
||||
"ast": "Asturian; Bable; Leonese; Asturleonese",
|
||||
"ath": "Athapascan languages",
|
||||
"aus": "Australian languages",
|
||||
"ava": "Avaric",
|
||||
"ave": "Avestan",
|
||||
"awa": "Awadhi",
|
||||
"aym": "Aymara",
|
||||
"aze": "Azerbaijani",
|
||||
"bad": "Banda languages",
|
||||
"bai": "Bamileke languages",
|
||||
"bak": "Bashkir",
|
||||
"bal": "Baluchi",
|
||||
"bam": "Bambara",
|
||||
"ban": "Balinese",
|
||||
"baq": "Basque",
|
||||
"bas": "Basa",
|
||||
"bat": "Baltic languages",
|
||||
"bej": "Beja; Bedawiyet",
|
||||
"bel": "Belarusian",
|
||||
"bem": "Bemba",
|
||||
"ben": "Bengali",
|
||||
"ber": "Berber languages",
|
||||
"bho": "Bhojpuri",
|
||||
"bih": "Bihari languages",
|
||||
"bik": "Bikol",
|
||||
"bin": "Bini; Edo",
|
||||
"bis": "Bislama",
|
||||
"bla": "Siksika",
|
||||
"bnt": "Bantu (Other)",
|
||||
"bos": "Bosnian",
|
||||
"bra": "Braj",
|
||||
"bre": "Breton",
|
||||
"btk": "Batak languages",
|
||||
"bua": "Buriat",
|
||||
"bug": "Buginese",
|
||||
"bul": "Bulgarian",
|
||||
"bur": "Burmese",
|
||||
"byn": "Blin; Bilin",
|
||||
"cad": "Caddo",
|
||||
"cai": "Central American Indian languages",
|
||||
"car": "Galibi Carib",
|
||||
"cat": "Catalan; Valencian",
|
||||
"cau": "Caucasian languages",
|
||||
"ceb": "Cebuano",
|
||||
"cel": "Celtic languages",
|
||||
"cha": "Chamorro",
|
||||
"chb": "Chibcha",
|
||||
"che": "Chechen",
|
||||
"chg": "Chagatai",
|
||||
"chi": "Chinese",
|
||||
"chk": "Chuukese",
|
||||
"chm": "Mari",
|
||||
"chn": "Chinook jargon",
|
||||
"cho": "Choctaw",
|
||||
"chp": "Chipewyan; Dene Suline",
|
||||
"chr": "Cherokee",
|
||||
"chu": "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic",
|
||||
"chv": "Chuvash",
|
||||
"chy": "Cheyenne",
|
||||
"cmc": "Chamic languages",
|
||||
"cop": "Coptic",
|
||||
"cor": "Cornish",
|
||||
"cos": "Corsican",
|
||||
"cpe": "Creoles and pidgins, English based",
|
||||
"cpf": "Creoles and pidgins, French-based ",
|
||||
"cpp": "Creoles and pidgins, Portuguese-based ",
|
||||
"cre": "Cree",
|
||||
"crh": "Crimean Tatar; Crimean Turkish",
|
||||
"crp": "Creoles and pidgins ",
|
||||
"csb": "Kashubian",
|
||||
"cus": "Cushitic languages",
|
||||
"cze": "Czech",
|
||||
"dak": "Dakota",
|
||||
"dan": "Danish",
|
||||
"dar": "Dargwa",
|
||||
"day": "Land Dayak languages",
|
||||
"del": "Delaware",
|
||||
"den": "Slave (Athapascan)",
|
||||
"dgr": "Dogrib",
|
||||
"din": "Dinka",
|
||||
"div": "Divehi; Dhivehi; Maldivian",
|
||||
"doi": "Dogri",
|
||||
"dra": "Dravidian languages",
|
||||
"dsb": "Lower Sorbian",
|
||||
"dua": "Duala",
|
||||
"dum": "Dutch, Middle (ca.1050-1350)",
|
||||
"dut": "Dutch; Flemish",
|
||||
"dyu": "Dyula",
|
||||
"dzo": "Dzongkha",
|
||||
"efi": "Efik",
|
||||
"egy": "Egyptian (Ancient)",
|
||||
"eka": "Ekajuk",
|
||||
"elx": "Elamite",
|
||||
"eng": "English",
|
||||
"enm": "English, Middle (1100-1500)",
|
||||
"epo": "Esperanto",
|
||||
"est": "Estonian",
|
||||
"ewe": "Ewe",
|
||||
"ewo": "Ewondo",
|
||||
"fan": "Fang",
|
||||
"fao": "Faroese",
|
||||
"fat": "Fanti",
|
||||
"fij": "Fijian",
|
||||
"fil": "Filipino; Pilipino",
|
||||
"fin": "Finnish",
|
||||
"fiu": "Finno-Ugrian languages",
|
||||
"fon": "Fon",
|
||||
"fre": "French",
|
||||
"frm": "French, Middle (ca.1400-1600)",
|
||||
"fro": "French, Old (842-ca.1400)",
|
||||
"frc": "French (Canada)",
|
||||
"frr": "Northern Frisian",
|
||||
"frs": "Eastern Frisian",
|
||||
"fry": "Western Frisian",
|
||||
"ful": "Fulah",
|
||||
"fur": "Friulian",
|
||||
"gaa": "Ga",
|
||||
"gay": "Gayo",
|
||||
"gba": "Gbaya",
|
||||
"gem": "Germanic languages",
|
||||
"geo": "Georgian",
|
||||
"ger": "German",
|
||||
"gez": "Geez",
|
||||
"gil": "Gilbertese",
|
||||
"gla": "Gaelic; Scottish Gaelic",
|
||||
"gle": "Irish",
|
||||
"glg": "Galician",
|
||||
"glv": "Manx",
|
||||
"gmh": "German, Middle High (ca.1050-1500)",
|
||||
"goh": "German, Old High (ca.750-1050)",
|
||||
"gon": "Gondi",
|
||||
"gor": "Gorontalo",
|
||||
"got": "Gothic",
|
||||
"grb": "Grebo",
|
||||
"grc": "Greek, Ancient (to 1453)",
|
||||
"gre": "Greek, Modern (1453-)",
|
||||
"grn": "Guarani",
|
||||
"gsw": "Swiss German; Alemannic; Alsatian",
|
||||
"guj": "Gujarati",
|
||||
"gwi": "Gwich'in",
|
||||
"hai": "Haida",
|
||||
"hat": "Haitian; Haitian Creole",
|
||||
"hau": "Hausa",
|
||||
"haw": "Hawaiian",
|
||||
"heb": "Hebrew",
|
||||
"her": "Herero",
|
||||
"hil": "Hiligaynon",
|
||||
"him": "Himachali languages; Western Pahari languages",
|
||||
"hin": "Hindi",
|
||||
"hit": "Hittite",
|
||||
"hmn": "Hmong; Mong",
|
||||
"hmo": "Hiri Motu",
|
||||
"hrv": "Croatian",
|
||||
"hsb": "Upper Sorbian",
|
||||
"hun": "Hungarian",
|
||||
"hup": "Hupa",
|
||||
"iba": "Iban",
|
||||
"ibo": "Igbo",
|
||||
"ice": "Icelandic",
|
||||
"ido": "Ido",
|
||||
"iii": "Sichuan Yi; Nuosu",
|
||||
"ijo": "Ijo languages",
|
||||
"iku": "Inuktitut",
|
||||
"ile": "Interlingue; Occidental",
|
||||
"ilo": "Iloko",
|
||||
"ina": "Interlingua (International Auxiliary Language Association)",
|
||||
"inc": "Indic languages",
|
||||
"ind": "Indonesian",
|
||||
"ine": "Indo-European languages",
|
||||
"inh": "Ingush",
|
||||
"ipk": "Inupiaq",
|
||||
"ira": "Iranian languages",
|
||||
"iro": "Iroquoian languages",
|
||||
"ita": "Italian",
|
||||
"jav": "Javanese",
|
||||
"jbo": "Lojban",
|
||||
"jpn": "Japanese",
|
||||
"jpr": "Judeo-Persian",
|
||||
"jrb": "Judeo-Arabic",
|
||||
"kaa": "Kara-Kalpak",
|
||||
"kab": "Kabyle",
|
||||
"kac": "Kachin; Jingpho",
|
||||
"kal": "Kalaallisut; Greenlandic",
|
||||
"kam": "Kamba",
|
||||
"kan": "Kannada",
|
||||
"kar": "Karen languages",
|
||||
"kas": "Kashmiri",
|
||||
"kau": "Kanuri",
|
||||
"kaw": "Kawi",
|
||||
"kaz": "Kazakh",
|
||||
"kbd": "Kabardian",
|
||||
"kha": "Khasi",
|
||||
"khi": "Khoisan languages",
|
||||
"khm": "Central Khmer",
|
||||
"kho": "Khotanese; Sakan",
|
||||
"kik": "Kikuyu; Gikuyu",
|
||||
"kin": "Kinyarwanda",
|
||||
"kir": "Kirghiz; Kyrgyz",
|
||||
"kmb": "Kimbundu",
|
||||
"kok": "Konkani",
|
||||
"kom": "Komi",
|
||||
"kon": "Kongo",
|
||||
"kor": "Korean",
|
||||
"kos": "Kosraean",
|
||||
"kpe": "Kpelle",
|
||||
"krc": "Karachay-Balkar",
|
||||
"krl": "Karelian",
|
||||
"kro": "Kru languages",
|
||||
"kru": "Kurukh",
|
||||
"kua": "Kuanyama; Kwanyama",
|
||||
"kum": "Kumyk",
|
||||
"kur": "Kurdish",
|
||||
"kut": "Kutenai",
|
||||
"lad": "Ladino",
|
||||
"lah": "Lahnda",
|
||||
"lam": "Lamba",
|
||||
"lao": "Lao",
|
||||
"lat": "Latin",
|
||||
"lav": "Latvian",
|
||||
"lez": "Lezghian",
|
||||
"lim": "Limburgan; Limburger; Limburgish",
|
||||
"lin": "Lingala",
|
||||
"lit": "Lithuanian",
|
||||
"lol": "Mongo",
|
||||
"loz": "Lozi",
|
||||
"ltz": "Luxembourgish; Letzeburgesch",
|
||||
"lua": "Luba-Lulua",
|
||||
"lub": "Luba-Katanga",
|
||||
"lug": "Ganda",
|
||||
"lui": "Luiseno",
|
||||
"lun": "Lunda",
|
||||
"luo": "Luo (Kenya and Tanzania)",
|
||||
"lus": "Lushai",
|
||||
"mac": "Macedonian",
|
||||
"mad": "Madurese",
|
||||
"mag": "Magahi",
|
||||
"mah": "Marshallese",
|
||||
"mai": "Maithili",
|
||||
"mak": "Makasar",
|
||||
"mal": "Malayalam",
|
||||
"man": "Mandingo",
|
||||
"mao": "Maori",
|
||||
"map": "Austronesian languages",
|
||||
"mar": "Marathi",
|
||||
"mas": "Masai",
|
||||
"may": "Malay",
|
||||
"mdf": "Moksha",
|
||||
"mdr": "Mandar",
|
||||
"men": "Mende",
|
||||
"mga": "Irish, Middle (900-1200)",
|
||||
"mic": "Mi'kmaq; Micmac",
|
||||
"min": "Minangkabau",
|
||||
"mis": "Uncoded languages",
|
||||
"mkh": "Mon-Khmer languages",
|
||||
"mlg": "Malagasy",
|
||||
"mlt": "Maltese",
|
||||
"mnc": "Manchu",
|
||||
"mni": "Manipuri",
|
||||
"mno": "Manobo languages",
|
||||
"moh": "Mohawk",
|
||||
"mon": "Mongolian",
|
||||
"mos": "Mossi",
|
||||
"mul": "Multiple languages",
|
||||
"mun": "Munda languages",
|
||||
"mus": "Creek",
|
||||
"mwl": "Mirandese",
|
||||
"mwr": "Marwari",
|
||||
"myn": "Mayan languages",
|
||||
"myv": "Erzya",
|
||||
"nah": "Nahuatl languages",
|
||||
"nai": "North American Indian languages",
|
||||
"nap": "Neapolitan",
|
||||
"nau": "Nauru",
|
||||
"nav": "Navajo; Navaho",
|
||||
"nbl": "Ndebele, South; South Ndebele",
|
||||
"nde": "Ndebele, North; North Ndebele",
|
||||
"ndo": "Ndonga",
|
||||
"nds": "Low German; Low Saxon; German, Low; Saxon, Low",
|
||||
"nep": "Nepali",
|
||||
"new": "Nepal Bhasa; Newari",
|
||||
"nia": "Nias",
|
||||
"nic": "Niger-Kordofanian languages",
|
||||
"niu": "Niuean",
|
||||
"nno": "Norwegian Nynorsk; Nynorsk, Norwegian",
|
||||
"nob": "Bokmål, Norwegian; Norwegian Bokmål",
|
||||
"nog": "Nogai",
|
||||
"non": "Norse, Old",
|
||||
"nor": "Norwegian",
|
||||
"nqo": "N'Ko",
|
||||
"nso": "Pedi; Sepedi; Northern Sotho",
|
||||
"nub": "Nubian languages",
|
||||
"nwc": "Classical Newari; Old Newari; Classical Nepal Bhasa",
|
||||
"nya": "Chichewa; Chewa; Nyanja",
|
||||
"nym": "Nyamwezi",
|
||||
"nyn": "Nyankole",
|
||||
"nyo": "Nyoro",
|
||||
"nzi": "Nzima",
|
||||
"oci": "Occitan (post 1500); Provençal",
|
||||
"oji": "Ojibwa",
|
||||
"ori": "Oriya",
|
||||
"orm": "Oromo",
|
||||
"osa": "Osage",
|
||||
"oss": "Ossetian; Ossetic",
|
||||
"ota": "Turkish, Ottoman (1500-1928)",
|
||||
"oto": "Otomian languages",
|
||||
"paa": "Papuan languages",
|
||||
"pag": "Pangasinan",
|
||||
"pal": "Pahlavi",
|
||||
"pam": "Pampanga; Kapampangan",
|
||||
"pan": "Panjabi; Punjabi",
|
||||
"pap": "Papiamento",
|
||||
"pau": "Palauan",
|
||||
"peo": "Persian, Old (ca.600-400 B.C.)",
|
||||
"per": "Persian",
|
||||
"phi": "Philippine languages",
|
||||
"phn": "Phoenician",
|
||||
"pli": "Pali",
|
||||
"pol": "Polish",
|
||||
"pon": "Pohnpeian",
|
||||
"por": "Portuguese",
|
||||
"pob": "Portuguese (Brazil)",
|
||||
"pra": "Prakrit languages",
|
||||
"pro": "Provençal, Old (to 1500)",
|
||||
"pus": "Pushto; Pashto",
|
||||
"qaa-qtz": "Reserved for local use",
|
||||
"que": "Quechua",
|
||||
"raj": "Rajasthani",
|
||||
"rap": "Rapanui",
|
||||
"rar": "Rarotongan; Cook Islands Maori",
|
||||
"roa": "Romance languages",
|
||||
"roh": "Romansh",
|
||||
"rom": "Romany",
|
||||
"rum": "Romanian; Moldavian; Moldovan",
|
||||
"run": "Rundi",
|
||||
"rup": "Aromanian; Arumanian; Macedo-Romanian",
|
||||
"rus": "Russian",
|
||||
"sad": "Sandawe",
|
||||
"sag": "Sango",
|
||||
"sah": "Yakut",
|
||||
"sai": "South American Indian (Other)",
|
||||
"sal": "Salishan languages",
|
||||
"sam": "Samaritan Aramaic",
|
||||
"san": "Sanskrit",
|
||||
"sas": "Sasak",
|
||||
"sat": "Santali",
|
||||
"scn": "Sicilian",
|
||||
"sco": "Scots",
|
||||
"sel": "Selkup",
|
||||
"sem": "Semitic languages",
|
||||
"sga": "Irish, Old (to 900)",
|
||||
"sgn": "Sign Languages",
|
||||
"shn": "Shan",
|
||||
"sid": "Sidamo",
|
||||
"sin": "Sinhala; Sinhalese",
|
||||
"sio": "Siouan languages",
|
||||
"sit": "Sino-Tibetan languages",
|
||||
"sla": "Slavic languages",
|
||||
"slo": "Slovak",
|
||||
"slv": "Slovenian",
|
||||
"sma": "Southern Sami",
|
||||
"sme": "Northern Sami",
|
||||
"smi": "Sami languages",
|
||||
"smj": "Lule Sami",
|
||||
"smn": "Inari Sami",
|
||||
"smo": "Samoan",
|
||||
"sms": "Skolt Sami",
|
||||
"sna": "Shona",
|
||||
"snd": "Sindhi",
|
||||
"snk": "Soninke",
|
||||
"sog": "Sogdian",
|
||||
"som": "Somali",
|
||||
"son": "Songhai languages",
|
||||
"sot": "Sotho, Southern",
|
||||
"spa": "Spanish; Latin",
|
||||
"spa": "Spanish; Castilian",
|
||||
"srd": "Sardinian",
|
||||
"srn": "Sranan Tongo",
|
||||
"srp": "Serbian",
|
||||
"srr": "Serer",
|
||||
"ssa": "Nilo-Saharan languages",
|
||||
"ssw": "Swati",
|
||||
"suk": "Sukuma",
|
||||
"sun": "Sundanese",
|
||||
"sus": "Susu",
|
||||
"sux": "Sumerian",
|
||||
"swa": "Swahili",
|
||||
"swe": "Swedish",
|
||||
"syc": "Classical Syriac",
|
||||
"syr": "Syriac",
|
||||
"tah": "Tahitian",
|
||||
"tai": "Tai languages",
|
||||
"tam": "Tamil",
|
||||
"tat": "Tatar",
|
||||
"tel": "Telugu",
|
||||
"tem": "Timne",
|
||||
"ter": "Tereno",
|
||||
"tet": "Tetum",
|
||||
"tgk": "Tajik",
|
||||
"tgl": "Tagalog",
|
||||
"tha": "Thai",
|
||||
"tib": "Tibetan",
|
||||
"tig": "Tigre",
|
||||
"tir": "Tigrinya",
|
||||
"tiv": "Tiv",
|
||||
"tkl": "Tokelau",
|
||||
"tlh": "Klingon; tlhIngan-Hol",
|
||||
"tli": "Tlingit",
|
||||
"tmh": "Tamashek",
|
||||
"tog": "Tonga (Nyasa)",
|
||||
"ton": "Tonga (Tonga Islands)",
|
||||
"tpi": "Tok Pisin",
|
||||
"tsi": "Tsimshian",
|
||||
"tsn": "Tswana",
|
||||
"tso": "Tsonga",
|
||||
"tuk": "Turkmen",
|
||||
"tum": "Tumbuka",
|
||||
"tup": "Tupi languages",
|
||||
"tur": "Turkish",
|
||||
"tut": "Altaic languages",
|
||||
"tvl": "Tuvalu",
|
||||
"twi": "Twi",
|
||||
"tyv": "Tuvinian",
|
||||
"udm": "Udmurt",
|
||||
"uga": "Ugaritic",
|
||||
"uig": "Uighur; Uyghur",
|
||||
"ukr": "Ukrainian",
|
||||
"umb": "Umbundu",
|
||||
"und": "Undetermined",
|
||||
"urd": "Urdu",
|
||||
"uzb": "Uzbek",
|
||||
"vai": "Vai",
|
||||
"ven": "Venda",
|
||||
"vie": "Vietnamese",
|
||||
"vol": "Volapük",
|
||||
"vot": "Votic",
|
||||
"wak": "Wakashan languages",
|
||||
"wal": "Walamo",
|
||||
"war": "Waray",
|
||||
"was": "Washo",
|
||||
"wel": "Welsh",
|
||||
"wen": "Sorbian languages",
|
||||
"wln": "Walloon",
|
||||
"wol": "Wolof",
|
||||
"xal": "Kalmyk; Oirat",
|
||||
"xho": "Xhosa",
|
||||
"yao": "Yao",
|
||||
"yap": "Yapese",
|
||||
"yid": "Yiddish",
|
||||
"yor": "Yoruba",
|
||||
"ypk": "Yupik languages",
|
||||
"zap": "Zapotec",
|
||||
"zbl": "Blissymbols; Blissymbolics; Bliss",
|
||||
"zen": "Zenaga",
|
||||
"zgh": "Standard Moroccan Tamazight",
|
||||
"zha": "Zhuang; Chuang",
|
||||
"znd": "Zande languages",
|
||||
"zul": "Zulu",
|
||||
"zun": "Zuni",
|
||||
"zxx": "No linguistic content; Not applicable",
|
||||
"zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
|
||||
}
|
||||
end function
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<interface>
|
||||
<field id="itemId" type="string" />
|
||||
<field id="selectedAudioStreamIndex" type="integer" value="0" />
|
||||
<field id="selectedSubtitleIndex" type="integer" value="-1" />
|
||||
<field id="selectedSubtitleIndex" type="integer" value="-2" />
|
||||
<field id="isIntro" type="boolean" />
|
||||
<field id="startIndex" type="integer" value="0" />
|
||||
<field id="itemType" type="string" value="" />
|
||||
|
|
|
@ -253,7 +253,8 @@ sub setMoviesOptions(options)
|
|||
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
|
||||
{ "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("RUNTIME"), "Name": "Runtime" }
|
||||
{ "Title": tr("RUNTIME"), "Name": "Runtime" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
|
||||
options.filter = [
|
||||
|
@ -272,7 +273,7 @@ sub setMoviesOptions(options)
|
|||
if m.options.view = "Studios" or m.view = "Studios"
|
||||
options.sort = [
|
||||
{ "Title": tr("TITLE"), "Name": "SortName" },
|
||||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
|
||||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" }
|
||||
]
|
||||
options.filter = [
|
||||
{ "Title": tr("All"), "Name": "All" },
|
||||
|
|
|
@ -140,14 +140,12 @@ sub loadInitialItems()
|
|||
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
|
||||
else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
m.top.showItemTitles = "hidealways"
|
||||
else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation"
|
||||
m.loadItemsTask.genreIds = ""
|
||||
m.top.showItemTitles = "hidealways"
|
||||
else
|
||||
m.loadItemsTask.itemId = m.top.parentItem.Id
|
||||
end if
|
||||
|
@ -228,6 +226,7 @@ sub setMusicOptions(options)
|
|||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
|
||||
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
|
||||
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
|
||||
options.filter = [
|
||||
|
@ -238,6 +237,7 @@ sub setMusicOptions(options)
|
|||
if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
|
||||
options.sort = [
|
||||
{ "Title": tr("TITLE"), "Name": "SortName" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
options.filter = []
|
||||
end if
|
||||
|
@ -246,6 +246,7 @@ sub setMusicOptions(options)
|
|||
options.sort = [
|
||||
{ "Title": tr("TITLE"), "Name": "SortName" },
|
||||
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
|
||||
{ "Title": tr("Random"), "Name": "Random" },
|
||||
]
|
||||
end if
|
||||
end sub
|
||||
|
|
|
@ -2,9 +2,10 @@ import "pkg:/source/utils/config.bs"
|
|||
|
||||
sub init()
|
||||
m.top.id = "overhang"
|
||||
' hide seperators till they're needed
|
||||
m.leftSeperator = m.top.findNode("overlayLeftSeperator")
|
||||
m.leftSeperator.visible = "false"
|
||||
m.top.translation = [54, 0]
|
||||
|
||||
m.leftGroup = m.top.findNode("overlayLeftGroup")
|
||||
m.rightGroup = m.top.findNode("overlayRightGroup")
|
||||
m.rightSeperator = m.top.findNode("overlayRightSeperator")
|
||||
' set font sizes
|
||||
m.optionText = m.top.findNode("overlayOptionsText")
|
||||
|
@ -38,7 +39,7 @@ end sub
|
|||
|
||||
sub onVisibleChange()
|
||||
if m.top.disableMoveAnimation
|
||||
m.top.translation = [0, 0]
|
||||
m.top.translation = [54, 0]
|
||||
return
|
||||
end if
|
||||
if m.top.isVisible
|
||||
|
@ -50,16 +51,7 @@ sub onVisibleChange()
|
|||
end sub
|
||||
|
||||
sub updateTitle()
|
||||
if m.top.title <> ""
|
||||
m.leftSeperator.visible = "true"
|
||||
else
|
||||
m.leftSeperator.visible = "false"
|
||||
end if
|
||||
m.title.text = m.top.title
|
||||
|
||||
if not m.hideClock
|
||||
resetTime()
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub setClockVisibility()
|
||||
|
@ -84,7 +76,9 @@ end sub
|
|||
sub updateUser()
|
||||
setRightSeperatorVisibility()
|
||||
user = m.top.findNode("overlayCurrentUser")
|
||||
user.text = m.top.currentUser
|
||||
if isValid(user)
|
||||
user.text = m.top.currentUser
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub updateTime()
|
||||
|
@ -145,3 +139,23 @@ sub updateOptions()
|
|||
m.optionStar.visible = false
|
||||
end if
|
||||
end sub
|
||||
|
||||
' component boolean field isLogoVisibleChange has changed value
|
||||
sub isLogoVisibleChange()
|
||||
isLogoVisible = m.top.isLogoVisible
|
||||
|
||||
scene = m.top.getScene()
|
||||
logo = scene.findNode("overlayLogo")
|
||||
|
||||
if isLogoVisible
|
||||
if not isValid(logo)
|
||||
posterLogo = createLogoPoster()
|
||||
m.leftGroup.insertChild(posterLogo, 0)
|
||||
end if
|
||||
else
|
||||
' remove the logo
|
||||
if isValid(logo)
|
||||
m.leftGroup.removeChild(logo)
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="JFOverhang" extends="Group">
|
||||
<children>
|
||||
<Poster id="overlayLogo" uri="pkg:/images/logo.png" translation="[70, 53]" width="270" height="72" />
|
||||
<LayoutGroup id="overlayLeftGroup" layoutDirection="horiz" translation="[375, 53]" itemSpacings="30">
|
||||
<Rectangle id="overlayLeftSeperator" color="#666666" width="2" height="64" />
|
||||
<LayoutGroup id="overlayLeftGroup" layoutDirection="horiz" translation="[54, 54]" itemSpacings="60">
|
||||
<Poster id="overlayLogo" uri="pkg:/images/logo.png" height="66" width="191" />
|
||||
<ScrollingLabel id="overlayTitle" font="font:LargeSystemFont" vertAlign="center" height="64" maxWidth="1100" repeatCount="0" />
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="overlayRightGroup" layoutDirection="horiz" itemSpacings="30" translation="[1820, 53]" horizAlignment="right">
|
||||
<LayoutGroup id="overlayRightGroup" layoutDirection="horiz" itemSpacings="30" translation="[1766, 53]" horizAlignment="right">
|
||||
<Label id="overlayCurrentUser" font="font:MediumSystemFont" width="300" horizAlign="right" vertAlign="center" height="64" />
|
||||
<Rectangle id="overlayRightSeperator" color="#666666" width="2" height="64" visible="false" />
|
||||
<LayoutGroup id="overlayTimeGroup" layoutDirection="horiz" horizAlignment="right" itemSpacings="0">
|
||||
|
@ -17,7 +16,7 @@
|
|||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
|
||||
<LayoutGroup layoutDirection="horiz" horizAlignment="right" translation="[1820, 125]" vertAlignment="custom">
|
||||
<LayoutGroup layoutDirection="horiz" horizAlignment="right" translation="[1766, 125]" vertAlignment="custom">
|
||||
<Label id="overlayOptionsStar" font="font:LargeSystemFont" text="*" />
|
||||
<Label id="overlayOptionsText" font="font:SmallSystemFont" text="Options" translation="[0,6]" />
|
||||
</LayoutGroup>
|
||||
|
@ -38,6 +37,6 @@
|
|||
<field id="showOptions" value="true" type="boolean" onChange="updateOptions" />
|
||||
<field id="isVisible" value="true" type="boolean" onChange="onVisibleChange" />
|
||||
<field id="disableMoveAnimation" value="false" type="boolean" />
|
||||
<function name="resetTime" />
|
||||
<field id="isLogoVisible" value="true" type="boolean" onChange="isLogoVisibleChange" />
|
||||
</interface>
|
||||
</component>
|
|
@ -8,6 +8,10 @@ sub init()
|
|||
m.poster = m.top.findNode("poster")
|
||||
m.unplayedCount = m.top.findNode("unplayedCount")
|
||||
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
|
||||
m.playedIndicator = m.top.findNode("playedIndicator")
|
||||
m.checkmark = m.top.findNode("checkmark")
|
||||
m.checkmark.width = 90
|
||||
m.checkmark.height = 60
|
||||
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
|
||||
|
@ -65,6 +69,10 @@ sub itemContentChanged() as void
|
|||
end if
|
||||
end if
|
||||
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
|
||||
m.playedIndicator.visible = true
|
||||
end if
|
||||
|
||||
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.IndexNumber)
|
||||
m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text
|
||||
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
<Rectangle id="backdrop" />
|
||||
<ScrollingLabel id="Series" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
|
||||
<Poster id="poster" translation="[2,0]" loadDisplayMode="scaleToFit">
|
||||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[104, 0]">
|
||||
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[102, 0]">
|
||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="90" height="60" translation="[102, 0]" visible="false" />
|
||||
</Poster>
|
||||
<ScrollingLabel id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
|
||||
<Label id="staticTitle" horizAlign="center" font="font:SmallSystemFont" wrap="false" />
|
||||
|
|
|
@ -109,7 +109,8 @@ sub onContentDataChanged()
|
|||
m.radioOptions.selectedIndex = i
|
||||
end if
|
||||
|
||||
textLine = cardItem.CreateChild("SimpleLabel")
|
||||
textLine = cardItem.CreateChild("ScrollingLabel")
|
||||
textLine.maxWidth = "750"
|
||||
textLine.text = item.track.description
|
||||
cardItem.observeField("selected", "onItemSelected")
|
||||
i++
|
||||
|
|
|
@ -22,6 +22,11 @@ sub init()
|
|||
"fontSize": 35,
|
||||
"fontUri": "font:SystemFontFile",
|
||||
"color": "#00a4dcFF"
|
||||
},
|
||||
"p": {
|
||||
"fontSize": 27,
|
||||
"fontUri": "font:SystemFontFile",
|
||||
"color": "#EFEFEFFF"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
m.top.setFocus(true)
|
||||
m.top.optionsAvailable = false
|
||||
end sub
|
||||
|
||||
' JFScreen hook.
|
||||
sub OnScreenShown()
|
||||
scene = m.top.getScene()
|
||||
overhang = scene.findNode("overhang")
|
||||
if isValid(overhang)
|
||||
overhang.isLogoVisible = true
|
||||
overhang.currentUser = ""
|
||||
end if
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
' Returns true if user navigates to a new focusable element
|
||||
if not press then return false
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="LoginScene" extends="JFGroup">
|
||||
<component name="LoginScene" extends="JFScreen">
|
||||
<children>
|
||||
<label text="Enter Configuration"
|
||||
id="prompt"
|
||||
|
|
|
@ -156,3 +156,14 @@ end function
|
|||
sub clearErrorMessage()
|
||||
m.top.errorMessage = ""
|
||||
end sub
|
||||
|
||||
' JFScreen hook called when the screen is displayed by the screen manager
|
||||
sub OnScreenShown()
|
||||
scene = m.top.getScene()
|
||||
overhang = scene.findNode("overhang")
|
||||
if isValid(overhang)
|
||||
overhang.isLogoVisible = true
|
||||
overhang.currentUser = ""
|
||||
overhang.title = ""
|
||||
end if
|
||||
end sub
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="SetServerScreen" extends="JFGroup">
|
||||
<component name="SetServerScreen" extends="JFScreen">
|
||||
<interface>
|
||||
<field id="serverUrl" type="string" alias="serverUrlTextbox.text" />
|
||||
<field id="serverWidth" alias="serverUrlOutline.width,serverUrlTextbox.width,serverUrlContainer.width,submitSizer.width" value="1620" />
|
||||
|
|
|
@ -31,7 +31,7 @@ sub setData()
|
|||
m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png"
|
||||
end if
|
||||
|
||||
else if datum.type = "Episode" or datum.type = "MusicVideo"
|
||||
else if datum.type = "Episode" or LCase(datum.type) = "recording" or datum.type = "MusicVideo"
|
||||
m.top.isWatched = datum.UserData.Played
|
||||
|
||||
imgParams = {}
|
||||
|
|
5
components/data/JFContentItem.bs
Normal file
5
components/data/JFContentItem.bs
Normal file
|
@ -0,0 +1,5 @@
|
|||
' Called whenever m.top.json changes.
|
||||
' It is expected that each node that extends JFContentItem will override this function
|
||||
sub setFields()
|
||||
|
||||
end sub
|
20
components/data/RecordingData.bs
Normal file
20
components/data/RecordingData.bs
Normal file
|
@ -0,0 +1,20 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub setFields()
|
||||
datum = m.top.json
|
||||
|
||||
m.top.id = datum.id
|
||||
m.top.title = datum.name
|
||||
m.top.showID = datum.SeriesID
|
||||
m.top.seasonID = datum.SeasonID
|
||||
m.top.overview = datum.overview
|
||||
m.top.favorite = datum.UserData.isFavorite
|
||||
end sub
|
||||
|
||||
sub setPoster()
|
||||
if isValid(m.top.image)
|
||||
m.top.posterURL = m.top.image.url
|
||||
else
|
||||
m.top.posterURL = ""
|
||||
end if
|
||||
end sub
|
18
components/data/RecordingData.xml
Normal file
18
components/data/RecordingData.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="RecordingData" extends="ContentNode">
|
||||
<interface>
|
||||
<field id="id" type="string" />
|
||||
<field id="title" type="string" />
|
||||
<field id="image" type="node" onChange="setPoster" />
|
||||
<field id="posterURL" type="string" />
|
||||
<field id="showID" type="string" />
|
||||
<field id="seasonID" type="string" />
|
||||
<field id="overview" type="string" />
|
||||
<field id="type" type="string" value="Recording" />
|
||||
<field id="startingPoint" type="longinteger" value="0" />
|
||||
<field id="json" type="assocarray" onChange="setFields" />
|
||||
<field id="selectedVideoStreamId" type="string" />
|
||||
<field id="selectedAudioStreamIndex" type="integer" />
|
||||
<field id="favorite" type="boolean" />
|
||||
</interface>
|
||||
</component>
|
|
@ -124,20 +124,18 @@ sub popScene()
|
|||
stopLoadingSpinner()
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Return group at top of stack without removing
|
||||
function getActiveScene() as object
|
||||
return m.groups.peek()
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Clear all content from group stack
|
||||
sub clearScenes()
|
||||
if m.content <> invalid then m.content.removeChildrenIndex(m.content.getChildCount(), 0)
|
||||
for each group in m.groups
|
||||
if LCase(group.subtype()) = "jfscreen"
|
||||
if type(group) = "roSGNode" and group.isSubtype("JFScreen")
|
||||
group.callFunc("OnScreenHidden")
|
||||
end if
|
||||
end for
|
||||
|
@ -191,35 +189,30 @@ sub registerOverhangData(group)
|
|||
end if
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Remove observers for overhang data
|
||||
sub unregisterOverhangData(group)
|
||||
group.unobserveField("overhangTitle")
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Update overhang title
|
||||
sub updateOverhangTitle(msg)
|
||||
m.overhang.title = msg.getData()
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Update options availability
|
||||
sub updateOptions(msg)
|
||||
m.overhang.showOptions = msg.getData()
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Update whether the overhang is visible or not
|
||||
sub updateOverhangVisible(msg)
|
||||
m.overhang.visible = msg.getData()
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Update username in overhang
|
||||
sub updateUser()
|
||||
|
@ -227,7 +220,6 @@ sub updateUser()
|
|||
if m.overhang <> invalid then m.overhang.currentUser = m.top.currentUser
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Reset time
|
||||
sub resetTime()
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
sub init()
|
||||
m.top.visible = true
|
||||
updateSize()
|
||||
|
@ -183,9 +185,20 @@ function buildRow(rowTitle as string, items, imgWdth = 0)
|
|||
row = CreateObject("roSGNode", "ContentNode")
|
||||
row.Title = tr(rowTitle)
|
||||
for each mov in items
|
||||
if LCase(mov.json.type) = "episode"
|
||||
if isAllValid([mov.json.SeriesName, mov.json.ParentIndexNumber, mov.json.IndexNumber, mov.json.Name])
|
||||
mov.labelText = mov.json.SeriesName
|
||||
mov.subTitle = `S${mov.json.ParentIndexNumber}:E${mov.json.IndexNumber} - ${mov.json.Name}`
|
||||
else
|
||||
mov.labelText = mov.json.Name
|
||||
mov.subTitle = mov.json.ProductionYear
|
||||
end if
|
||||
else
|
||||
mov.labelText = mov.json.Name
|
||||
mov.subTitle = mov.json.ProductionYear
|
||||
end if
|
||||
|
||||
mov.Id = mov.json.Id
|
||||
mov.labelText = mov.json.Name
|
||||
mov.subTitle = mov.json.ProductionYear
|
||||
mov.Type = mov.json.Type
|
||||
if imgWdth > 0
|
||||
mov.imageWidth = imgWdth
|
||||
|
|
|
@ -30,9 +30,16 @@ sub loadLibraries()
|
|||
m.fadeInFocusBitmap.control = "start"
|
||||
end sub
|
||||
|
||||
' JFScreen hook that gets ran as needed.
|
||||
' Used to update the focus, the state of the data, and tells the server about the device profile
|
||||
' JFScreen hook called when the screen is displayed by the screen manager
|
||||
sub OnScreenShown()
|
||||
scene = m.top.getScene()
|
||||
overhang = scene.findNode("overhang")
|
||||
if isValid(overhang)
|
||||
overhang.isLogoVisible = true
|
||||
overhang.currentUser = m.global.session.user.name
|
||||
overhang.title = tr("Home")
|
||||
end if
|
||||
|
||||
if isValid(m.top.lastFocus)
|
||||
m.top.lastFocus.setFocus(true)
|
||||
else
|
||||
|
@ -53,6 +60,17 @@ sub OnScreenShown()
|
|||
end if
|
||||
end sub
|
||||
|
||||
' JFScreen hook called when the screen is hidden by the screen manager
|
||||
sub OnScreenHidden()
|
||||
scene = m.top.getScene()
|
||||
overhang = scene.findNode("overhang")
|
||||
if isValid(overhang)
|
||||
overhang.isLogoVisible = false
|
||||
overhang.currentUser = ""
|
||||
overhang.title = ""
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Triggered by m.postTask after completing a post.
|
||||
' Empty the task data when finished.
|
||||
sub postFinished()
|
||||
|
|
|
@ -21,16 +21,25 @@ sub init()
|
|||
m.showProgressBarField = m.top.findNode("showProgressBarField")
|
||||
|
||||
' Randomize the background colors
|
||||
backdropColor = "#00a4db" ' set default in case global var is invalid
|
||||
localGlobal = m.global
|
||||
|
||||
if isValid(localGlobal) and isValid(localGlobal.constants) and isValid(localGlobal.constants.poster_bg_pallet)
|
||||
posterBackgrounds = localGlobal.constants.poster_bg_pallet
|
||||
backdropColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
end if
|
||||
|
||||
' update the backdrop node
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
posterBackgrounds = m.global.constants.poster_bg_pallet
|
||||
m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
m.backdrop.color = backdropColor
|
||||
end sub
|
||||
|
||||
|
||||
sub itemContentChanged()
|
||||
m.unplayedCount.visible = false
|
||||
if isValid(m.unplayedCount) then m.unplayedCount.visible = false
|
||||
itemData = m.top.itemContent
|
||||
if itemData = invalid then return
|
||||
localGlobal = m.global
|
||||
|
||||
itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem"
|
||||
|
||||
|
@ -48,16 +57,17 @@ sub itemContentChanged()
|
|||
|
||||
if itemData.isWatched
|
||||
m.playedIndicator.visible = true
|
||||
m.unplayedCount.visible = false
|
||||
else
|
||||
m.playedIndicator.visible = false
|
||||
|
||||
if LCase(itemData.type) = "series"
|
||||
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
if isValid(localGlobal) and isValid(localGlobal.session) and isValid(localGlobal.session.user) and isValid(localGlobal.session.user.settings)
|
||||
if not localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"]
|
||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
if isValid(m.unplayedCount) then m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
@ -113,7 +123,7 @@ sub itemContentChanged()
|
|||
return
|
||||
end if
|
||||
|
||||
if itemData.type = "Episode"
|
||||
if itemData.type = "Episode" or LCase(itemData.type) = "recording"
|
||||
m.itemText.text = itemData.json.SeriesName
|
||||
|
||||
if itemData.PlayedPercentage > 0
|
||||
|
|
|
@ -75,12 +75,17 @@ sub processUserSections()
|
|||
m.expectedRowCount = 1 ' the favorites row is hardcoded to always show atm
|
||||
m.processedRowCount = 0
|
||||
|
||||
sessionUser = m.global.session.user
|
||||
|
||||
' calculate expected row count by processing homesections
|
||||
for i = 0 to 6
|
||||
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
||||
userSection = sessionUser.settings["homesection" + i.toStr()]
|
||||
sectionName = userSection ?? "none"
|
||||
sectionName = LCase(sectionName)
|
||||
|
||||
if sectionName = "latestmedia"
|
||||
' expect 1 row per filtered media library
|
||||
m.filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
|
||||
m.filteredLatest = filterNodeArray(m.libraryData, "id", sessionUser.configuration.LatestItemsExcludes)
|
||||
for each latestLibrary in m.filteredLatest
|
||||
if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program"
|
||||
m.expectedRowCount++
|
||||
|
@ -94,7 +99,10 @@ sub processUserSections()
|
|||
' Add home sections in order based on user settings
|
||||
loadedSections = 0
|
||||
for i = 0 to 6
|
||||
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
||||
userSection = sessionUser.settings["homesection" + i.toStr()]
|
||||
sectionName = userSection ?? "none"
|
||||
sectionName = LCase(sectionName)
|
||||
|
||||
sectionLoaded = false
|
||||
if sectionName <> "none"
|
||||
sectionLoaded = addHomeSection(sectionName)
|
||||
|
@ -141,8 +149,13 @@ function getOriginalSectionIndex(sectionName as string) as integer
|
|||
sectionIndex = 0
|
||||
indexLatestMediaSection = 0
|
||||
|
||||
sessionUser = m.global.session.user
|
||||
|
||||
for i = 0 to 6
|
||||
settingSectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
||||
userSection = sessionUser.settings["homesection" + i.toStr()]
|
||||
settingSectionName = userSection ?? "none"
|
||||
settingSectionName = LCase(settingSectionName)
|
||||
|
||||
if settingSectionName = "latestmedia"
|
||||
indexLatestMediaSection = i
|
||||
end if
|
||||
|
|
|
@ -24,6 +24,16 @@ sub redraw()
|
|||
m.top.findNode("UserRow").translation = [leftBorder, topBorder]
|
||||
end sub
|
||||
|
||||
' JFScreen hook called when the screen is displayed by the screen manager
|
||||
sub OnScreenShown()
|
||||
scene = m.top.getScene()
|
||||
overhang = scene.findNode("overhang")
|
||||
if isValid(overhang)
|
||||
overhang.isLogoVisible = true
|
||||
overhang.currentUser = ""
|
||||
end if
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if not press then return false
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="UserSelect" extends="JFGroup">
|
||||
<component name="UserSelect" extends="JFScreen">
|
||||
<children>
|
||||
<Label text="Please sign in" horizAlign="center" font="font:LargeSystemFont" height="100" width="1920" translation="[0, 200]" />
|
||||
<UserRow id="userRow" translation="[130, 360]" />
|
||||
|
|
|
@ -125,6 +125,11 @@ sub playQueue()
|
|||
return
|
||||
end if
|
||||
|
||||
if nextItemMediaType = "audiobook"
|
||||
CreateAudioPlayerView()
|
||||
return
|
||||
end if
|
||||
|
||||
if nextItemMediaType = "musicvideo"
|
||||
CreateVideoPlayerView()
|
||||
return
|
||||
|
@ -145,6 +150,11 @@ sub playQueue()
|
|||
return
|
||||
end if
|
||||
|
||||
if nextItemMediaType = "recording"
|
||||
CreateVideoPlayerView()
|
||||
return
|
||||
end if
|
||||
|
||||
if nextItemMediaType = "trailer"
|
||||
CreateVideoPlayerView()
|
||||
return
|
||||
|
@ -249,6 +259,10 @@ sub setTopStartingPoint(positionTicks)
|
|||
m.queue[0].startingPoint = positionTicks
|
||||
end sub
|
||||
|
||||
' getItemType: Returns the media type of the passed item
|
||||
'
|
||||
' @param {dynamic} item - Item to evaluate
|
||||
' @return {string} indicating type of media item is
|
||||
function getItemType(item) as string
|
||||
if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
|
||||
return LCase(item.json.mediatype)
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<function name="getHold" />
|
||||
<function name="getIsShuffled" />
|
||||
<function name="getItemByIndex" />
|
||||
<function name="getItemType" />
|
||||
<function name="getPosition" />
|
||||
<function name="getQueue" />
|
||||
<function name="getQueueTypes" />
|
||||
|
|
|
@ -14,6 +14,7 @@ sub CreateVideoPlayerView()
|
|||
m.view.observeField("state", "onStateChange")
|
||||
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
|
||||
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
|
||||
m.view.observeField("selectAudioPressed", "onSelectAudioPressed")
|
||||
|
||||
mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").mediaSourceId
|
||||
|
||||
|
@ -32,18 +33,40 @@ end sub
|
|||
' Event Handlers
|
||||
' -----------------
|
||||
|
||||
|
||||
' onSelectAudioPressed: Display audio selection dialog
|
||||
'
|
||||
sub onSelectAudioPressed()
|
||||
audioData = {
|
||||
data: []
|
||||
}
|
||||
|
||||
for each item in m.view.fullAudioData
|
||||
|
||||
audioStreamItem = {
|
||||
"Index": item.Index,
|
||||
"IsExternal": item.IsExternal,
|
||||
"Track": {
|
||||
"description": item.DisplayTitle
|
||||
},
|
||||
"Type": "audioselection"
|
||||
}
|
||||
|
||||
if m.view.audioIndex = item.Index
|
||||
audioStreamItem.selected = true
|
||||
end if
|
||||
|
||||
audioData.data.push(audioStreamItem)
|
||||
end for
|
||||
|
||||
m.global.sceneManager.callFunc("radioDialog", tr("Select Audio"), audioData)
|
||||
m.global.sceneManager.observeField("returnData", "onSelectionMade")
|
||||
end sub
|
||||
|
||||
' User requested subtitle selection popup
|
||||
sub onSelectSubtitlePressed()
|
||||
' None is always first in the subtitle list
|
||||
subtitleData = {
|
||||
data: [{
|
||||
"Index": -1,
|
||||
"IsExternal": false,
|
||||
"Track": {
|
||||
"description": "None"
|
||||
},
|
||||
"Type": "subtitleselection"
|
||||
}]
|
||||
data: []
|
||||
}
|
||||
|
||||
for each item in m.view.fullSubtitleData
|
||||
|
@ -69,9 +92,24 @@ sub onSelectSubtitlePressed()
|
|||
end if
|
||||
end if
|
||||
|
||||
subtitleData.data.push(item)
|
||||
' Put the selected item at the top of the option list
|
||||
if isValid(item.selected) and item.selected
|
||||
subtitleData.data.Unshift(item)
|
||||
else
|
||||
subtitleData.data.push(item)
|
||||
end if
|
||||
end for
|
||||
|
||||
' Manually create the None option and place at top
|
||||
subtitleData.data.Unshift({
|
||||
"Index": -1,
|
||||
"IsExternal": false,
|
||||
"Track": {
|
||||
"description": "None"
|
||||
},
|
||||
"Type": "subtitleselection"
|
||||
})
|
||||
|
||||
m.global.sceneManager.callFunc("radioDialog", tr("Select Subtitles"), subtitleData)
|
||||
m.global.sceneManager.observeField("returnData", "onSelectionMade")
|
||||
end sub
|
||||
|
@ -85,6 +123,25 @@ sub onSelectionMade()
|
|||
|
||||
if LCase(m.global.sceneManager.returnData.type) = "subtitleselection"
|
||||
processSubtitleSelection()
|
||||
return
|
||||
end if
|
||||
|
||||
if LCase(m.global.sceneManager.returnData.type) = "audioselection"
|
||||
processAudioSelection()
|
||||
return
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
' processAudioSelection: Audio track selection handler
|
||||
'
|
||||
sub processAudioSelection()
|
||||
selectedAudioTrack = m.global.sceneManager.returnData
|
||||
|
||||
if isValid(selectedAudioTrack)
|
||||
if isValid(selectedAudioTrack.index)
|
||||
m.view.audioIndex = selectedAudioTrack.index
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -99,6 +156,14 @@ sub processSubtitleSelection()
|
|||
' The playbackData is now outdated and must be refreshed
|
||||
m.playbackData = invalid
|
||||
|
||||
' Find previously selected subtitle and identify if it was encoded
|
||||
for each item in m.view.fullSubtitleData
|
||||
if item.index = m.view.selectedSubtitle
|
||||
m.view.previousSubtitleWasEncoded = item.IsEncoded
|
||||
exit for
|
||||
end if
|
||||
end for
|
||||
|
||||
if LCase(m.selectedSubtitle.track.description) = "none"
|
||||
m.view.globalCaptionMode = "Off"
|
||||
m.view.subtitleTrack = ""
|
||||
|
@ -111,19 +176,20 @@ sub processSubtitleSelection()
|
|||
end if
|
||||
|
||||
if m.selectedSubtitle.IsEncoded
|
||||
' Roku can not natively display these subtitles, so turn off the caption mode on the device
|
||||
m.view.globalCaptionMode = "Off"
|
||||
else
|
||||
' Roku can natively display these subtitles, ensure the caption mode on the device is on
|
||||
m.view.globalCaptionMode = "On"
|
||||
end if
|
||||
|
||||
if m.selectedSubtitle.IsExternal
|
||||
' Roku may rearrange subtitle tracks. Look up track based on name to ensure we get the correct index
|
||||
availableSubtitleTrackIndex = availSubtitleTrackIdx(m.selectedSubtitle.Track.TrackName)
|
||||
if availableSubtitleTrackIndex = -1 then return
|
||||
|
||||
m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
|
||||
else
|
||||
m.view.selectedSubtitle = m.selectedSubtitle.Index
|
||||
end if
|
||||
|
||||
m.view.selectedSubtitle = m.selectedSubtitle.Index
|
||||
end sub
|
||||
|
||||
' User requested playback info
|
||||
|
|
|
@ -14,6 +14,7 @@ sub init()
|
|||
m.songList.observeField("doneLoading", "onDoneLoading")
|
||||
|
||||
m.dscr = m.top.findNode("overview")
|
||||
m.dscr.ellipsisText = tr("... (Press * to read more)")
|
||||
createDialogPallete()
|
||||
end sub
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<JFButton id="instantMix" minChars="8" text="Instant Mix"></JFButton>
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="infoGroup" layoutDirection="vert" itemSpacings="[15]">
|
||||
<Label id="overview" wrap="true" height="310" width="1250" ellipsisText=" ... (Press * to read more)" />
|
||||
<Label id="overview" wrap="true" height="310" width="1250" />
|
||||
<Rectangle id='songListRect' translation="[-30, 0]" width="1260" height="510" color="0x202020ff">
|
||||
<AlbumTrackList itemComponentName="SongItem" id="songList" translation="[45, 25]" itemSize="[1170,60]" numRows="7" />
|
||||
</Rectangle>
|
||||
|
|
|
@ -39,6 +39,7 @@ sub init()
|
|||
m.backDrop = m.top.findNode("backdrop")
|
||||
m.artistImage = m.top.findNode("artistImage")
|
||||
m.dscr = m.top.findNode("overview")
|
||||
m.dscr.ellipsisText = tr("... (Press * to read more)")
|
||||
m.dscr.observeField("isTextEllipsized", "onEllipsisChanged")
|
||||
createDialogPallete()
|
||||
end sub
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[75]">
|
||||
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[125]">
|
||||
<LayoutGroup layoutDirection="vert" itemSpacings="[75]">
|
||||
<Label id="overview" wrap="true" lineSpacing="25" maxLines="6" width="1080" ellipsisText=" ... (Press * to read more)" />
|
||||
<Label id="overview" wrap="true" lineSpacing="25" maxLines="6" width="1080" />
|
||||
<ButtonGroupHoriz id="buttons" itemSpacings="[20]">
|
||||
<IconButton id="play" background="#070707" focusBackground="#00a4dc" padding="35" icon="pkg:/images/icons/play.png" text="Play" height="85" width="150" />
|
||||
<IconButton id="instantMix" background="#070707" focusBackground="#00a4dc" padding="35" icon="pkg:/images/icons/instantMix.png" text="Instant Mix" height="85" width="150" />
|
||||
|
|
|
@ -5,6 +5,11 @@ import "pkg:/source/utils/config.bs"
|
|||
|
||||
sub init()
|
||||
m.top.optionsAvailable = false
|
||||
m.inScrubMode = false
|
||||
m.lastRecordedPositionTimestamp = 0
|
||||
m.scrubTimestamp = -1
|
||||
|
||||
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
|
||||
|
||||
setupAudioNode()
|
||||
setupAnimationTasks()
|
||||
|
@ -13,9 +18,8 @@ sub init()
|
|||
setupDataTasks()
|
||||
setupScreenSaver()
|
||||
|
||||
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
|
||||
|
||||
m.buttonCount = m.buttons.getChildCount()
|
||||
m.seekPosition.translation = [720 - (m.seekPosition.width / 2), m.seekPosition.translation[1]]
|
||||
|
||||
m.screenSaverTimeout = 300
|
||||
|
||||
|
@ -32,6 +36,8 @@ sub init()
|
|||
pageContentChanged()
|
||||
setShuffleIconState()
|
||||
setLoopButtonImage()
|
||||
|
||||
m.buttons.setFocus(true)
|
||||
end sub
|
||||
|
||||
sub onScreensaverTimeoutLoaded()
|
||||
|
@ -96,6 +102,20 @@ end sub
|
|||
sub setupButtons()
|
||||
m.buttons = m.top.findNode("buttons")
|
||||
m.top.observeField("selectedButtonIndex", "onButtonSelectedChange")
|
||||
|
||||
' If we're playing a mixed playlist, remove the shuffle and loop buttons
|
||||
if m.playlistTypeCount > 1
|
||||
shuffleButton = m.top.findNode("shuffle")
|
||||
m.buttons.removeChild(shuffleButton)
|
||||
|
||||
loopButton = m.top.findNode("loop")
|
||||
m.buttons.removeChild(loopButton)
|
||||
|
||||
m.previouslySelectedButtonIndex = 0
|
||||
m.top.selectedButtonIndex = 1
|
||||
return
|
||||
end if
|
||||
|
||||
m.previouslySelectedButtonIndex = 1
|
||||
m.top.selectedButtonIndex = 2
|
||||
end sub
|
||||
|
@ -117,13 +137,18 @@ sub setupInfoNodes()
|
|||
m.playPosition = m.top.findNode("playPosition")
|
||||
m.bufferPosition = m.top.findNode("bufferPosition")
|
||||
m.seekBar = m.top.findNode("seekBar")
|
||||
m.thumb = m.top.findNode("thumb")
|
||||
m.shuffleIndicator = m.top.findNode("shuffleIndicator")
|
||||
m.loopIndicator = m.top.findNode("loopIndicator")
|
||||
m.positionTimestamp = m.top.findNode("positionTimestamp")
|
||||
m.seekPosition = m.top.findNode("seekPosition")
|
||||
m.seekTimestamp = m.top.findNode("seekTimestamp")
|
||||
m.totalLengthTimestamp = m.top.findNode("totalLengthTimestamp")
|
||||
end sub
|
||||
|
||||
sub bufferPositionChanged()
|
||||
if m.inScrubMode then return
|
||||
|
||||
if not isValid(m.global.audioPlayer.bufferingStatus)
|
||||
bufferPositionBarWidth = m.seekBar.width
|
||||
else
|
||||
|
@ -141,6 +166,8 @@ sub bufferPositionChanged()
|
|||
end sub
|
||||
|
||||
sub audioPositionChanged()
|
||||
stopLoadingSpinner()
|
||||
|
||||
if m.global.audioPlayer.position = 0
|
||||
m.playPosition.width = 0
|
||||
end if
|
||||
|
@ -159,14 +186,22 @@ sub audioPositionChanged()
|
|||
playPositionBarWidth = m.seekBar.width
|
||||
end if
|
||||
|
||||
if not m.inScrubMode
|
||||
moveSeekbarThumb(playPositionBarWidth)
|
||||
' Change the seek position timestamp
|
||||
m.seekTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false)
|
||||
end if
|
||||
|
||||
' Use animation to make the display smooth
|
||||
m.playPositionAnimationWidth.keyValue = [m.playPosition.width, playPositionBarWidth]
|
||||
m.playPositionAnimation.control = "start"
|
||||
|
||||
' Update displayed position timestamp
|
||||
if isValid(m.global.audioPlayer.position)
|
||||
m.lastRecordedPositionTimestamp = m.global.audioPlayer.position
|
||||
m.positionTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false)
|
||||
else
|
||||
m.lastRecordedPositionTimestamp = 0
|
||||
m.positionTimestamp.text = "0:00"
|
||||
end if
|
||||
|
||||
|
@ -217,7 +252,9 @@ sub audioStateChanged()
|
|||
if m.global.audioPlayer.state = "finished"
|
||||
' User has enabled single song loop, play current song again
|
||||
if m.global.audioPlayer.loopMode = "one"
|
||||
m.scrubTimestamp = -1
|
||||
playAction()
|
||||
exitScrubMode()
|
||||
return
|
||||
end if
|
||||
|
||||
|
@ -261,8 +298,28 @@ function playAction() as boolean
|
|||
end function
|
||||
|
||||
function previousClicked() as boolean
|
||||
if m.playlistTypeCount > 1 then return false
|
||||
if m.global.queueManager.callFunc("getPosition") = 0 then return false
|
||||
currentQueuePosition = m.global.queueManager.callFunc("getPosition")
|
||||
|
||||
if currentQueuePosition = 0 then return false
|
||||
|
||||
if m.playlistTypeCount > 1
|
||||
previousItem = m.global.queueManager.callFunc("getItemByIndex", currentQueuePosition - 1)
|
||||
previousItemType = m.global.queueManager.callFunc("getItemType", previousItem)
|
||||
|
||||
if previousItemType <> "audio"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
m.global.queueManager.callFunc("moveBack")
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
exitScrubMode()
|
||||
|
||||
m.lastRecordedPositionTimestamp = 0
|
||||
m.positionTimestamp.text = "0:00"
|
||||
|
||||
if m.global.audioPlayer.state = "playing"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
|
@ -276,7 +333,6 @@ function previousClicked() as boolean
|
|||
m.global.queueManager.callFunc("moveBack")
|
||||
pageContentChanged()
|
||||
|
||||
|
||||
return true
|
||||
end function
|
||||
|
||||
|
@ -312,7 +368,28 @@ sub setLoopButtonImage()
|
|||
end sub
|
||||
|
||||
function nextClicked() as boolean
|
||||
if m.playlistTypeCount > 1 then return false
|
||||
if m.playlistTypeCount > 1
|
||||
currentQueuePosition = m.global.queueManager.callFunc("getPosition")
|
||||
if currentQueuePosition < m.global.queueManager.callFunc("getCount") - 1
|
||||
|
||||
nextItem = m.global.queueManager.callFunc("getItemByIndex", currentQueuePosition + 1)
|
||||
nextItemType = m.global.queueManager.callFunc("getItemType", nextItem)
|
||||
|
||||
if nextItemType <> "audio"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
m.global.queueManager.callFunc("moveForward")
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
exitScrubMode()
|
||||
|
||||
m.lastRecordedPositionTimestamp = 0
|
||||
m.positionTimestamp.text = "0:00"
|
||||
|
||||
' Reset loop mode due to manual user interaction
|
||||
if m.global.audioPlayer.loopMode = "one"
|
||||
|
@ -379,6 +456,8 @@ sub LoadNextSong()
|
|||
m.global.audioPlayer.control = "stop"
|
||||
end if
|
||||
|
||||
exitScrubMode()
|
||||
|
||||
' Reset playPosition bar without animation
|
||||
m.playPosition.width = 0
|
||||
m.global.queueManager.callFunc("moveForward")
|
||||
|
@ -388,46 +467,10 @@ end sub
|
|||
' Update values on screen when page content changes
|
||||
sub pageContentChanged()
|
||||
|
||||
' Reset buffer bar without animation
|
||||
m.bufferPosition.width = 0
|
||||
m.LoadAudioStreamTask.control = "STOP"
|
||||
|
||||
useMetaTask = false
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
|
||||
if not isValid(currentItem.RunTimeTicks)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.AlbumArtist)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.name)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.Artists)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if useMetaTask
|
||||
m.LoadMetaDataTask.itemId = currentItem.id
|
||||
m.LoadMetaDataTask.observeField("content", "onMetaDataLoaded")
|
||||
m.LoadMetaDataTask.control = "RUN"
|
||||
else
|
||||
if isValid(currentItem.ParentBackdropItemId)
|
||||
setBackdropImage(ImageURL(currentItem.ParentBackdropItemId, "Backdrop", { "maxHeight": "720", "maxWidth": "1280" }))
|
||||
end if
|
||||
|
||||
setPosterImage(ImageURL(currentItem.id, "Primary", { "maxHeight": 500, "maxWidth": 500 }))
|
||||
setScreenTitle(currentItem)
|
||||
setOnScreenTextValues(currentItem)
|
||||
m.songDuration = currentItem.RunTimeTicks / 10000000.0
|
||||
|
||||
' Update displayed total audio length
|
||||
m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks)
|
||||
end if
|
||||
|
||||
m.LoadAudioStreamTask.itemId = currentItem.id
|
||||
m.LoadAudioStreamTask.observeField("content", "onAudioStreamLoaded")
|
||||
m.LoadAudioStreamTask.control = "RUN"
|
||||
|
@ -435,9 +478,6 @@ end sub
|
|||
|
||||
' If we have more and 1 song to play, fade in the next and previous controls
|
||||
sub loadButtons()
|
||||
' Don't show audio buttons if we have a mixed playlist
|
||||
if m.playlistTypeCount > 1 then return
|
||||
|
||||
if m.global.queueManager.callFunc("getCount") > 1
|
||||
m.shuffleIndicator.opacity = ".4"
|
||||
m.loopIndicator.opacity = ".4"
|
||||
|
@ -451,6 +491,46 @@ sub onAudioStreamLoaded()
|
|||
data = m.LoadAudioStreamTask.content[0]
|
||||
m.LoadAudioStreamTask.unobserveField("content")
|
||||
if data <> invalid and data.count() > 0
|
||||
' Reset buffer bar without animation
|
||||
m.bufferPosition.width = 0
|
||||
|
||||
useMetaTask = false
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
|
||||
if not isValid(currentItem.RunTimeTicks)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.AlbumArtist)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.name)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if not isValid(currentItem.Artists)
|
||||
useMetaTask = true
|
||||
end if
|
||||
|
||||
if useMetaTask
|
||||
m.LoadMetaDataTask.itemId = currentItem.id
|
||||
m.LoadMetaDataTask.observeField("content", "onMetaDataLoaded")
|
||||
m.LoadMetaDataTask.control = "RUN"
|
||||
else
|
||||
if isValid(currentItem.ParentBackdropItemId)
|
||||
setBackdropImage(ImageURL(currentItem.ParentBackdropItemId, "Backdrop", { "maxHeight": "720", "maxWidth": "1280" }))
|
||||
end if
|
||||
|
||||
setPosterImage(ImageURL(currentItem.id, "Primary", { "maxHeight": 500, "maxWidth": 500 }))
|
||||
setScreenTitle(currentItem)
|
||||
setOnScreenTextValues(currentItem)
|
||||
m.songDuration = currentItem.RunTimeTicks / 10000000.0
|
||||
|
||||
' Update displayed total audio length
|
||||
m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks)
|
||||
end if
|
||||
|
||||
m.global.audioPlayer.content = data
|
||||
m.global.audioPlayer.control = "none"
|
||||
m.global.audioPlayer.control = "play"
|
||||
|
@ -540,6 +620,96 @@ sub setBackdropImage(data)
|
|||
end if
|
||||
end sub
|
||||
|
||||
' setSelectedButtonState: Changes the icon state url for the currently selected button
|
||||
'
|
||||
' @param {string} oldState - current state to replace in icon url
|
||||
' @param {string} newState - state to replace {oldState} with in icon url
|
||||
sub setSelectedButtonState(oldState as string, newState as string)
|
||||
selectedButton = m.buttons.getChild(m.top.selectedButtonIndex)
|
||||
selectedButton.uri = selectedButton.uri.Replace(oldState, newState)
|
||||
end sub
|
||||
|
||||
' processScrubAction: Handles +/- seeking for the audio trickplay bar
|
||||
'
|
||||
' @param {integer} seekStep - seconds to move the trickplay position (negative values allowed)
|
||||
sub processScrubAction(seekStep as integer)
|
||||
' Prepare starting playStart property value
|
||||
if m.scrubTimestamp = -1
|
||||
m.scrubTimestamp = m.lastRecordedPositionTimestamp
|
||||
end if
|
||||
|
||||
' Don't let seek to go past the end of the song
|
||||
if m.scrubTimestamp + seekStep > m.songDuration - 5
|
||||
return
|
||||
end if
|
||||
|
||||
if seekStep > 0
|
||||
' Move seek forward
|
||||
m.scrubTimestamp += seekStep
|
||||
else if m.scrubTimestamp >= Abs(seekStep)
|
||||
' If back seek won't go below 0, move seek back
|
||||
m.scrubTimestamp += seekStep
|
||||
else
|
||||
' Back seek would go below 0, set to 0 directly
|
||||
m.scrubTimestamp = 0
|
||||
end if
|
||||
|
||||
' Move the seedbar thumb forward
|
||||
songPercentComplete = m.scrubTimestamp / m.songDuration
|
||||
playPositionBarWidth = m.seekBar.width * songPercentComplete
|
||||
|
||||
moveSeekbarThumb(playPositionBarWidth)
|
||||
|
||||
' Change the displayed position timestamp
|
||||
m.seekTimestamp.text = secondsToHuman(m.scrubTimestamp, false)
|
||||
end sub
|
||||
|
||||
' resetSeekbarThumb: Resets the thumb to the playing position
|
||||
'
|
||||
sub resetSeekbarThumb()
|
||||
m.scrubTimestamp = -1
|
||||
moveSeekbarThumb(m.playPosition.width)
|
||||
end sub
|
||||
|
||||
' moveSeekbarThumb: Positions the thumb on the seekbar
|
||||
'
|
||||
' @param {float} playPositionBarWidth - width of the play position bar
|
||||
sub moveSeekbarThumb(playPositionBarWidth as float)
|
||||
' Center the thumb on the play position bar
|
||||
thumbPostionLeft = playPositionBarWidth - 10
|
||||
|
||||
' Don't let thumb go below 0
|
||||
if thumbPostionLeft < 0 then thumbPostionLeft = 0
|
||||
|
||||
' Don't let thumb go past end of seekbar
|
||||
if thumbPostionLeft > m.seekBar.width - 25
|
||||
thumbPostionLeft = m.seekBar.width - 25
|
||||
end if
|
||||
|
||||
' Move the thumb
|
||||
m.thumb.translation = [thumbPostionLeft, m.thumb.translation[1]]
|
||||
|
||||
' Move the seek position element so it follows the thumb
|
||||
m.seekPosition.translation = [720 + thumbPostionLeft - (m.seekPosition.width / 2), m.seekPosition.translation[1]]
|
||||
end sub
|
||||
|
||||
' exitScrubMode: Moves player out of scrub mode state, resets back to standard play mode
|
||||
'
|
||||
sub exitScrubMode()
|
||||
m.buttons.setFocus(true)
|
||||
m.thumb.setFocus(false)
|
||||
|
||||
if m.seekPosition.visible
|
||||
m.seekPosition.visible = false
|
||||
end if
|
||||
|
||||
resetSeekbarThumb()
|
||||
|
||||
m.inScrubMode = false
|
||||
m.thumb.visible = false
|
||||
setSelectedButtonState("-default", "-selected")
|
||||
end sub
|
||||
|
||||
' Process key press events
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
|
||||
|
@ -551,9 +721,58 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
return true
|
||||
end if
|
||||
|
||||
' Key Event handler when m.thumb is in focus
|
||||
if m.thumb.hasFocus()
|
||||
if key = "right"
|
||||
m.inScrubMode = true
|
||||
processScrubAction(10)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "left"
|
||||
m.inScrubMode = true
|
||||
processScrubAction(-10)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "OK" or key = "play"
|
||||
if m.inScrubMode
|
||||
startLoadingSpinner()
|
||||
m.inScrubMode = false
|
||||
m.global.audioPlayer.seek = m.scrubTimestamp
|
||||
return true
|
||||
end if
|
||||
|
||||
return playAction()
|
||||
end if
|
||||
end if
|
||||
|
||||
if key = "play"
|
||||
return playAction()
|
||||
else if key = "back"
|
||||
end if
|
||||
|
||||
if key = "up"
|
||||
if not m.thumb.visible
|
||||
m.thumb.visible = true
|
||||
setSelectedButtonState("-selected", "-default")
|
||||
end if
|
||||
if not m.seekPosition.visible
|
||||
m.seekPosition.visible = true
|
||||
end if
|
||||
|
||||
m.thumb.setFocus(true)
|
||||
m.buttons.setFocus(false)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "down"
|
||||
if m.thumb.visible
|
||||
exitScrubMode()
|
||||
end if
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "back"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
m.global.audioPlayer.loopMode = ""
|
||||
else if key = "rewind"
|
||||
|
@ -561,30 +780,36 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
else if key = "fastforward"
|
||||
return nextClicked()
|
||||
else if key = "left"
|
||||
if m.global.queueManager.callFunc("getCount") = 1 then return false
|
||||
if m.buttons.hasFocus()
|
||||
if m.global.queueManager.callFunc("getCount") = 1 then return false
|
||||
|
||||
if m.top.selectedButtonIndex > 0
|
||||
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
|
||||
m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1
|
||||
if m.top.selectedButtonIndex > 0
|
||||
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
|
||||
m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1
|
||||
end if
|
||||
return true
|
||||
end if
|
||||
return true
|
||||
else if key = "right"
|
||||
if m.global.queueManager.callFunc("getCount") = 1 then return false
|
||||
if m.buttons.hasFocus()
|
||||
if m.global.queueManager.callFunc("getCount") = 1 then return false
|
||||
|
||||
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
|
||||
if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
|
||||
return true
|
||||
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
|
||||
if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
|
||||
return true
|
||||
end if
|
||||
else if key = "OK"
|
||||
if m.buttons.getChild(m.top.selectedButtonIndex).id = "play"
|
||||
return playAction()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "previous"
|
||||
return previousClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "next"
|
||||
return nextClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "shuffle"
|
||||
return shuffleClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "loop"
|
||||
return loopClicked()
|
||||
if m.buttons.hasFocus()
|
||||
if m.buttons.getChild(m.top.selectedButtonIndex).id = "play"
|
||||
return playAction()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "previous"
|
||||
return previousClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "next"
|
||||
return nextClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "shuffle"
|
||||
return shuffleClicked()
|
||||
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "loop"
|
||||
return loopClicked()
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
<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" />
|
||||
<Label id="positionTimestamp" width="100" height="25" horizAlign="right" font="font:SmallestSystemFont" translation="[590,838]" color="#999999" text="0:00" />
|
||||
<Label id="totalLengthTimestamp" width="100" height="25" horizAlign="left" font="font:SmallestSystemFont" translation="[1230,838]" 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" />
|
||||
|
@ -16,6 +17,7 @@
|
|||
<Rectangle id="seekBar" color="0x00000099" width="500" height="10">
|
||||
<Rectangle id="bufferPosition" color="0xFFFFFF44" height="10"></Rectangle>
|
||||
<Rectangle id="playPosition" color="#00a4dcFF" height="10"></Rectangle>
|
||||
<Poster id="thumb" width="25" height="25" uri="pkg:/images/icons/circle.png" visible="false" translation="[0, -10]" />
|
||||
</Rectangle>
|
||||
<LayoutGroup id="buttons" layoutDirection="horiz" horizAlignment="center" itemSpacings="[45]">
|
||||
<Poster id="loop" width="64" height="64" uri="pkg:/images/icons/loop-default.png" opacity="0" />
|
||||
|
@ -37,6 +39,9 @@
|
|||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, 1.0]" fieldToInterp="loop.opacity" />
|
||||
</Animation>
|
||||
</LayoutGroup>
|
||||
<Rectangle id="seekPosition" visible="false" color="0x00000090" height="40" width="110" translation="[720, 790]">
|
||||
<Label text="0:00" id="seekTimestamp" width="110" height="40" horizAlign="center" vertAlign="center" font="font:SmallestSystemFont" />
|
||||
</Rectangle>
|
||||
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" visible="false" />
|
||||
<Poster id="screenSaverAlbumCover" width="500" height="500" translation="[960,575]" opacity="0" />
|
||||
<Poster id="PosterOne" width="389" height="104" translation="[960,540]" opacity="0" />
|
||||
|
|
|
@ -13,6 +13,7 @@ sub init()
|
|||
m.songList.observeField("doneLoading", "onDoneLoading")
|
||||
|
||||
m.dscr = m.top.findNode("overview")
|
||||
m.dscr.ellipsisText = tr("... (Press * to read more)")
|
||||
createDialogPallete()
|
||||
end sub
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<JFButton id="playAll" minChars="8" text="Play All"></JFButton>
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="infoGroup" layoutDirection="vert" itemSpacings="[15]">
|
||||
<Label id="overview" wrap="true" height="310" width="1250" ellipsisText=" ... (Press * to read more)" />
|
||||
<Label id="overview" wrap="true" height="310" width="1250" />
|
||||
<Rectangle id='songListRect' translation="[-30, 0]" width="1260" height="510" color="0x202020ff">
|
||||
<AlbumTrackList itemComponentName="SongItem" id="songList" translation="[45, 25]" itemSize="[1170,60]" numRows="7" />
|
||||
</Rectangle>
|
||||
|
|
|
@ -10,8 +10,16 @@ sub monitorQuickConnect()
|
|||
authenticated = checkQuickConnect(m.top.secret)
|
||||
|
||||
if authenticated = true
|
||||
m.top.authenticated = 1
|
||||
else
|
||||
m.top.authenticated = -1
|
||||
loggedIn = AuthenticateViaQuickConnect(m.top.secret)
|
||||
if loggedIn
|
||||
currentUser = AboutMe()
|
||||
session.user.Login(currentUser, m.top.saveCredentials)
|
||||
session.user.LoadUserPreferences()
|
||||
LoadUserAbilities()
|
||||
m.top.authenticated = 1
|
||||
return
|
||||
end if
|
||||
end if
|
||||
|
||||
m.top.authenticated = -1
|
||||
end sub
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
<interface>
|
||||
<field id="secret" type="string" />
|
||||
<field id="authenticated" type="integer" value="0" />
|
||||
<field id="saveCredentials" type="boolean" />
|
||||
</interface>
|
||||
</component>
|
|
@ -14,11 +14,13 @@ sub quickConnectStatus()
|
|||
m.quickConnectTimer.control = "stop"
|
||||
m.checkTask = CreateObject("roSGNode", "QuickConnect")
|
||||
m.checkTask.secret = m.top.quickConnectJson.secret
|
||||
m.checkTask.saveCredentials = m.top.saveCredentials
|
||||
m.checkTask.observeField("authenticated", "OnAuthenticated")
|
||||
m.checkTask.control = "run"
|
||||
end sub
|
||||
|
||||
sub OnAuthenticated()
|
||||
m.checkTask.control = "stop"
|
||||
m.checkTask.unobserveField("authenticated")
|
||||
|
||||
' Did we get the A-OK to authenticate?
|
||||
|
@ -26,28 +28,18 @@ sub OnAuthenticated()
|
|||
if authenticated < 0
|
||||
' Still waiting, check again in 3 seconds...
|
||||
authenticated = 0
|
||||
m.checkTask.observeField("authenticated", "OnAuthenticated")
|
||||
m.quickConnectTimer.control = "start"
|
||||
else if authenticated > 0
|
||||
' We've been given the go ahead, try to authenticate via Quick Connect...
|
||||
authenticated = AuthenticateViaQuickConnect(m.top.quickConnectJson.secret)
|
||||
if authenticated <> invalid and authenticated = true
|
||||
currentUser = AboutMe()
|
||||
session.user.Login(currentUser, m.top.saveCredentials)
|
||||
session.user.LoadUserPreferences()
|
||||
LoadUserAbilities()
|
||||
m.top.close = true
|
||||
m.top.authenticated = true
|
||||
else
|
||||
m.top.close = true
|
||||
m.top.authenticated = false
|
||||
end if
|
||||
' We've been logged in via Quick Connect...
|
||||
m.top.close = true
|
||||
m.top.authenticated = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub quickConnectClosed()
|
||||
m.quickConnectTimer.control = "stop"
|
||||
if m.checkTask <> invalid
|
||||
m.checkTask.control = "stop"
|
||||
m.checkTask.unobserveField("authenticated")
|
||||
end if
|
||||
m.top.close = true
|
||||
|
|
|
@ -93,6 +93,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
focusedItem = getFocusedItem()
|
||||
if isValid(focusedItem)
|
||||
m.top.selectedItem = focusedItem
|
||||
'Prevent the selected item event from double firing
|
||||
m.top.selectedItem = invalid
|
||||
end if
|
||||
return true
|
||||
end if
|
||||
|
|
|
@ -28,7 +28,10 @@ sub itemContentChanged()
|
|||
end if
|
||||
|
||||
if isValid(itemData.indexNumber)
|
||||
indexNumber = itemData.indexNumber.toStr() + ". "
|
||||
indexNumber = `${itemData.indexNumber}. `
|
||||
if isValid(itemData.indexNumberEnd)
|
||||
indexNumber = `${itemData.indexNumber}-${itemData.indexNumberEnd}. `
|
||||
end if
|
||||
else
|
||||
indexNumber = ""
|
||||
end if
|
||||
|
|
|
@ -12,6 +12,9 @@ sub init()
|
|||
m.Shuffle = m.top.findNode("Shuffle")
|
||||
m.extrasSlider.visible = true
|
||||
m.seasons = m.top.findNode("seasons")
|
||||
m.overview = m.top.findNode("overview")
|
||||
|
||||
m.overview.ellipsisText = tr("... (Press * to read more)")
|
||||
end sub
|
||||
|
||||
sub itemContentChanged()
|
||||
|
@ -194,11 +197,20 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
|
||||
if not press then return false
|
||||
|
||||
overview = m.top.findNode("overview")
|
||||
if key = "options"
|
||||
if m.overview.isTextEllipsized
|
||||
if isAllValid([m.top.itemContent.json.name, m.top.itemContent.json.overview])
|
||||
m.global.sceneManager.callFunc("standardDialog", m.top.itemContent.json.name, { data: ["<p>" + m.top.itemContent.json.overview + "</p>"] })
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
return false
|
||||
end if
|
||||
|
||||
topGrp = m.top.findNode("seasons")
|
||||
bottomGrp = m.top.findNode("extrasGrid")
|
||||
|
||||
if key = "down" and overview.hasFocus()
|
||||
if key = "down" and m.overview.hasFocus()
|
||||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
else if key = "down" and m.Shuffle.hasFocus()
|
||||
|
@ -222,7 +234,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
else if key = "up" and m.Shuffle.hasFocus()
|
||||
overview.setFocus(true)
|
||||
m.overview.setFocus(true)
|
||||
return true
|
||||
else if key = "play" and m.seasons.hasFocus()
|
||||
print "play was pressed from the seasons row"
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import "pkg:/source/utils/misc.bs"
|
||||
|
||||
const LOGO_RIGHT_PADDING = 30
|
||||
const OPTIONCONTROLS_TOP_PADDING = 50
|
||||
|
||||
sub init()
|
||||
m.videoControls = m.top.findNode("videoControls")
|
||||
m.optionControls = m.top.findNode("optionControls")
|
||||
|
||||
m.inactivityTimer = m.top.findNode("inactivityTimer")
|
||||
m.videoInfo = m.top.findNode("videoInfo")
|
||||
m.itemTitle = m.top.findNode("itemTitle")
|
||||
m.videoPlayPause = m.top.findNode("videoPlayPause")
|
||||
m.videoPositionTime = m.top.findNode("videoPositionTime")
|
||||
|
@ -16,10 +20,16 @@ sub init()
|
|||
m.top.observeField("hasFocus", "onFocusChanged")
|
||||
m.top.observeField("progressPercentage", "onProgressPercentageChanged")
|
||||
m.top.observeField("playbackState", "onPlaybackStateChanged")
|
||||
m.top.observeField("itemTitleText", "onItemTitleTextChanged")
|
||||
|
||||
m.defaultButtonIndex = 1
|
||||
m.focusedButtonIndex = 1
|
||||
m.top.observeField("itemTitleText", "onItemTitleTextChanged")
|
||||
m.top.observeField("seasonNumber", "onSeasonNumberChanged")
|
||||
m.top.observeField("episodeNumber", "onEpisodeNumberChanged")
|
||||
m.top.observeField("episodeNumberEnd", "onEpisodeNumberEndChanged")
|
||||
m.top.observeField("logoImage", "onLogoImageChanged")
|
||||
|
||||
m.defaultButtonIndex = 2
|
||||
m.focusedButtonIndex = 2
|
||||
m.optionControlsMoved = false
|
||||
|
||||
m.videoControls.buttonFocused = m.defaultButtonIndex
|
||||
m.optionControls.buttonFocused = m.optionControls.getChildCount() - 1
|
||||
|
@ -53,6 +63,92 @@ sub onItemTitleTextChanged()
|
|||
m.itemTitle.text = m.top.itemTitleText
|
||||
end sub
|
||||
|
||||
' onSeasonNumberChanged: Handler for changes to m.top.seasonNumber param.
|
||||
'
|
||||
sub onSeasonNumberChanged()
|
||||
m.top.unobserveField("seasonNumber")
|
||||
itemSeason = m.top.findNode("itemSeason")
|
||||
itemSeason.font.size = 32
|
||||
itemSeason.text = `S${m.top.seasonNumber}`
|
||||
|
||||
' Move the option controls down to give room for season number
|
||||
if not m.optionControlsMoved
|
||||
moveOptionControls(0, OPTIONCONTROLS_TOP_PADDING)
|
||||
m.optionControlsMoved = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
' onEpisodeNumberChanged: Handler for changes to m.top.episodeNumber param.
|
||||
'
|
||||
sub onEpisodeNumberChanged()
|
||||
m.top.unobserveField("episodeNumber")
|
||||
itemEpisode = m.top.findNode("itemEpisode")
|
||||
itemEpisode.font.size = 32
|
||||
itemEpisode.text = `E${m.top.episodeNumber}`
|
||||
|
||||
' Move the option controls down to give room for episode number
|
||||
if not m.optionControlsMoved
|
||||
moveOptionControls(0, OPTIONCONTROLS_TOP_PADDING)
|
||||
m.optionControlsMoved = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
' onEpisodeNumberEndChanged: Handler for changes to m.top.episodeNumberEnd param.
|
||||
'
|
||||
sub onEpisodeNumberEndChanged()
|
||||
m.top.unobserveField("episodeNumberEnd")
|
||||
itemEpisodeEnd = m.top.findNode("itemEpisodeEnd")
|
||||
itemEpisodeEnd.font.size = 32
|
||||
itemEpisodeEnd.text = `-${m.top.episodeNumberEnd}`
|
||||
|
||||
' Move the option controls down to give room for episode number
|
||||
if not m.optionControlsMoved
|
||||
moveOptionControls(0, OPTIONCONTROLS_TOP_PADDING)
|
||||
m.optionControlsMoved = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
' moveOptionControls: Moves option controls node based on passed pixel values
|
||||
'
|
||||
' @param {integer} horizontalPixels - Number of horizontal pixels to move option controls
|
||||
' @param {integer} verticalPixels - Number of vertical pixels to move option controls
|
||||
sub moveOptionControls(horizontalPixels as integer, verticalPixels as integer)
|
||||
m.optionControls.translation = `[${m.optionControls.translation[0] + horizontalPixels}, ${m.optionControls.translation[1] + verticalPixels}]`
|
||||
end sub
|
||||
|
||||
' onLogoLoadStatusChanged: Handler for changes to logo image's status.
|
||||
'
|
||||
' @param {dynamic} event - field change event
|
||||
sub onLogoLoadStatusChanged(event)
|
||||
if LCase(event.GetData()) = "ready"
|
||||
logoImage = event.getRoSGNode()
|
||||
logoImage.unobserveField("loadStatus")
|
||||
|
||||
' Move video info to the right based on the logo width
|
||||
m.videoInfo.translation = `[${m.videoInfo.translation[0] + logoImage.bitmapWidth + LOGO_RIGHT_PADDING}, ${m.videoInfo.translation[1]}]`
|
||||
m.itemTitle.maxWidth = m.itemTitle.maxWidth - (logoImage.bitmapWidth + LOGO_RIGHT_PADDING)
|
||||
|
||||
' Move the option controls down based on the logo height
|
||||
if not m.optionControlsMoved
|
||||
moveOptionControls(0, OPTIONCONTROLS_TOP_PADDING)
|
||||
m.optionControlsMoved = true
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
' onLogoImageChanged: Handler for changes to m.top.logoImage param.
|
||||
'
|
||||
sub onLogoImageChanged()
|
||||
if isValidAndNotEmpty(m.top.logoImage)
|
||||
logoImage = createObject("roSGNode", "Poster")
|
||||
logoImage.Id = "logoImage"
|
||||
logoImage.observeField("loadStatus", "onLogoLoadStatusChanged")
|
||||
logoImage.uri = m.top.logoImage
|
||||
logoImage.translation = [103, 61]
|
||||
m.top.appendChild(logoImage)
|
||||
end if
|
||||
end sub
|
||||
|
||||
' resetFocusToDefaultButton: Reset focus back to the default button
|
||||
'
|
||||
sub resetFocusToDefaultButton()
|
||||
|
@ -75,7 +171,7 @@ sub resetFocusToDefaultButton()
|
|||
m.videoControls.setFocus(true)
|
||||
m.focusedButtonIndex = m.defaultButtonIndex
|
||||
m.videoControls.getChild(m.defaultButtonIndex).focus = true
|
||||
m.videoControls.buttonFocused = 1
|
||||
m.videoControls.buttonFocused = m.defaultButtonIndex
|
||||
m.optionControls.buttonFocused = m.optionControls.getChildCount() - 1
|
||||
end sub
|
||||
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="OSD" extends="Group" initialFocus="chapterNext">
|
||||
<children>
|
||||
<ScrollingLabel id="itemTitle" font="font:LargeBoldSystemFont" translation="[103,61]" maxWidth="1400" />
|
||||
<Poster uri="pkg:/images/osdBackground.png" width="1920" height="279" />
|
||||
|
||||
<LayoutGroup id="videoInfo" layoutDirection="vert" translation="[103,61]">
|
||||
<ScrollingLabel id="itemTitle" font="font:LargeBoldSystemFont" maxWidth="1400" />
|
||||
<LayoutGroup id="videoInfo" layoutDirection="horiz" translation="[103,61]">
|
||||
<Label id="itemSeason" font="font:MediumSystemFont" color="0xffffffFF" />
|
||||
<Label id="itemEpisode" font="font:MediumSystemFont" color="0xffffffFF" />
|
||||
<Label id="itemEpisodeEnd" font="font:MediumSystemFont" color="0xffffffFF" />
|
||||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
|
||||
<Clock id="clock" translation="[1618, 46]" />
|
||||
|
||||
<ButtonGroup id="optionControls" itemSpacings="[20]" layoutDirection="horiz" horizAlignment="left" translation="[103,120]">
|
||||
<IconButton id="showVideoInfoPopup" background="#070707" focusBackground="#00a4dc" padding="16" icon="pkg:/images/icons/videoInfo.png" height="65" width="100" />
|
||||
<IconButton id="chapterList" background="#070707" focusBackground="#00a4dc" padding="16" icon="pkg:/images/icons/numberList.png" height="65" width="100" />
|
||||
<IconButton id="showSubtitleMenu" background="#070707" focusBackground="#00a4dc" padding="0" icon="pkg:/images/icons/subtitle.png" height="65" width="100" />
|
||||
<IconButton id="showAudioMenu" background="#070707" focusBackground="#00a4dc" padding="27" icon="pkg:/images/icons/musicNote.png" height="65" width="100" />
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup id="videoControls" itemSpacings="[20]" layoutDirection="horiz" horizAlignment="center" translation="[960,875]">
|
||||
<IconButton id="itemBack" background="#070707" focusBackground="#00a4dc" padding="35" icon="pkg:/images/icons/itemPrevious.png" height="65" width="100" />
|
||||
<IconButton id="chapterBack" background="#070707" focusBackground="#00a4dc" padding="16" icon="pkg:/images/icons/previousChapter.png" height="65" width="100" />
|
||||
<IconButton id="videoPlayPause" background="#070707" focusBackground="#00a4dc" padding="35" icon="pkg:/images/icons/play.png" height="65" width="100" />
|
||||
<IconButton id="chapterNext" background="#070707" focusBackground="#00a4dc" padding="16" icon="pkg:/images/icons/nextChapter.png" height="65" width="100" />
|
||||
<IconButton id="itemNext" background="#070707" focusBackground="#00a4dc" padding="35" icon="pkg:/images/icons/itemNext.png" height="65" width="100" />
|
||||
</ButtonGroup>
|
||||
|
||||
<Rectangle id="progressBarBackground" color="0x00000098" width="1714" height="8" translation="[103,970]">
|
||||
|
@ -27,6 +40,10 @@
|
|||
</children>
|
||||
<interface>
|
||||
<field id="itemTitleText" type="string" />
|
||||
<field id="seasonNumber" type="integer" />
|
||||
<field id="episodeNumber" type="integer" />
|
||||
<field id="episodeNumberEnd" type="integer" />
|
||||
<field id="logoImage" type="string" />
|
||||
<field id="inactiveTimeout" type="integer" />
|
||||
<field id="progressPercentage" type="float" />
|
||||
<field id="positionTime" type="float" />
|
||||
|
|
|
@ -32,6 +32,7 @@ sub init()
|
|||
m.top.observeField("state", "onState")
|
||||
m.top.observeField("content", "onContentChange")
|
||||
m.top.observeField("selectedSubtitle", "onSubtitleChange")
|
||||
m.top.observeField("audioIndex", "onAudioIndexChange")
|
||||
|
||||
' Custom Caption Function
|
||||
m.top.observeField("allowCaptions", "onAllowCaptionsChange")
|
||||
|
@ -91,6 +92,35 @@ sub handleChapterSkipAction(action as string)
|
|||
end if
|
||||
end sub
|
||||
|
||||
' handleItemSkipAction: Handles user command to skip items
|
||||
'
|
||||
' @param {string} action - skip action to take
|
||||
sub handleItemSkipAction(action as string)
|
||||
if action = "itemnext"
|
||||
' If there is something next in the queue, play it
|
||||
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
||||
m.top.control = "stop"
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
m.global.queueManager.callFunc("moveForward")
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
end if
|
||||
|
||||
return
|
||||
end if
|
||||
|
||||
if action = "itemback"
|
||||
' If there is something previous in the queue, play it
|
||||
if m.global.queueManager.callFunc("getPosition") > 0
|
||||
m.top.control = "stop"
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
m.global.queueManager.callFunc("moveBack")
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
end if
|
||||
|
||||
return
|
||||
end if
|
||||
end sub
|
||||
|
||||
' handleHideAction: Handles action to hide OSD menu
|
||||
'
|
||||
' @param {boolean} resume - controls whether or not to resume video playback when sub is called
|
||||
|
@ -163,6 +193,12 @@ sub handleShowSubtitleMenuAction()
|
|||
m.top.selectSubtitlePressed = true
|
||||
end sub
|
||||
|
||||
' handleShowAudioMenuAction: Handles action to show audio selection menu
|
||||
'
|
||||
sub handleShowAudioMenuAction()
|
||||
m.top.selectAudioPressed = true
|
||||
end sub
|
||||
|
||||
' handleShowVideoInfoPopupAction: Handles action to show video info popup
|
||||
'
|
||||
sub handleShowVideoInfoPopupAction()
|
||||
|
@ -204,10 +240,20 @@ sub onOSDAction()
|
|||
return
|
||||
end if
|
||||
|
||||
if action = "showaudiomenu"
|
||||
handleShowAudioMenuAction()
|
||||
return
|
||||
end if
|
||||
|
||||
if action = "showvideoinfopopup"
|
||||
handleShowVideoInfoPopupAction()
|
||||
return
|
||||
end if
|
||||
|
||||
if action = "itemback" or action = "itemnext"
|
||||
handleItemSkipAction(action)
|
||||
return
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Only setup caption items if captions are allowed
|
||||
|
@ -256,12 +302,42 @@ end sub
|
|||
|
||||
' Event handler for when selectedSubtitle changes
|
||||
sub onSubtitleChange()
|
||||
switchWithoutRefresh = true
|
||||
|
||||
if m.top.SelectedSubtitle <> -1
|
||||
' If the global caption mode is off, then Roku can't display the subtitles natively and needs a video stop/start
|
||||
if LCase(m.top.globalCaptionMode) <> "on" then switchWithoutRefresh = false
|
||||
end if
|
||||
|
||||
' If previous sustitle was encoded, then we need to a video stop/start to change subtitle content
|
||||
if m.top.previousSubtitleWasEncoded then switchWithoutRefresh = false
|
||||
|
||||
if switchWithoutRefresh then return
|
||||
|
||||
' Save the current video position
|
||||
m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
|
||||
|
||||
m.top.control = "stop"
|
||||
|
||||
m.LoadMetaDataTask.selectedSubtitleIndex = m.top.SelectedSubtitle
|
||||
m.LoadMetaDataTask.selectedAudioStreamIndex = m.top.audioIndex
|
||||
m.LoadMetaDataTask.itemId = m.currentItem.id
|
||||
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
|
||||
m.LoadMetaDataTask.control = "RUN"
|
||||
end sub
|
||||
|
||||
' Event handler for when audioIndex changes
|
||||
sub onAudioIndexChange()
|
||||
' Skip initial audio index setting
|
||||
if m.top.position = 0 then return
|
||||
|
||||
' Save the current video position
|
||||
m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
|
||||
|
||||
m.top.control = "stop"
|
||||
|
||||
m.LoadMetaDataTask.selectedSubtitleIndex = m.top.SelectedSubtitle
|
||||
m.LoadMetaDataTask.selectedAudioStreamIndex = m.top.audioIndex
|
||||
m.LoadMetaDataTask.itemId = m.currentItem.id
|
||||
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
|
||||
m.LoadMetaDataTask.control = "RUN"
|
||||
|
@ -316,12 +392,39 @@ sub onVideoContentLoaded()
|
|||
m.top.container = videoContent[0].container
|
||||
m.top.mediaSourceId = videoContent[0].mediaSourceId
|
||||
m.top.fullSubtitleData = videoContent[0].fullSubtitleData
|
||||
m.top.fullAudioData = videoContent[0].fullAudioData
|
||||
m.top.audioIndex = videoContent[0].audioIndex
|
||||
m.top.transcodeParams = videoContent[0].transcodeparams
|
||||
m.chapters = videoContent[0].chapters
|
||||
m.top.showID = videoContent[0].showID
|
||||
|
||||
m.osd.itemTitleText = m.top.content.title
|
||||
|
||||
' If video is an episode, attempt to add season and episode numbers to OSD
|
||||
if m.top.content.contenttype = 4
|
||||
if isValid(videoContent[0].seasonNumber)
|
||||
m.osd.seasonNumber = videoContent[0].seasonNumber
|
||||
end if
|
||||
|
||||
if isValid(videoContent[0].episodeNumber)
|
||||
m.osd.episodeNumber = videoContent[0].episodeNumber
|
||||
end if
|
||||
|
||||
if isValid(videoContent[0].episodeNumberEnd)
|
||||
m.osd.episodeNumberEnd = videoContent[0].episodeNumberEnd
|
||||
end if
|
||||
end if
|
||||
|
||||
' Attempt to add logo to OSD
|
||||
if isValidAndNotEmpty(videoContent[0].logoImage)
|
||||
m.osd.logoImage = videoContent[0].logoImage
|
||||
|
||||
' Don't show both the logo and the video title if this isn't an episode
|
||||
if m.top.content.contenttype <> 4
|
||||
m.osd.itemTitleText = ""
|
||||
end if
|
||||
end if
|
||||
|
||||
populateChapterMenu()
|
||||
|
||||
if m.LoadMetaDataTask.isIntro
|
||||
|
@ -332,6 +435,32 @@ sub onVideoContentLoaded()
|
|||
m.top.allowCaptions = true
|
||||
end if
|
||||
|
||||
' Allow default subtitles
|
||||
m.top.unobserveField("selectedSubtitle")
|
||||
|
||||
' Set subtitleTrack property if subs are natively supported by Roku
|
||||
selectedSubtitle = invalid
|
||||
for each subtitle in m.top.fullSubtitleData
|
||||
if subtitle.Index = videoContent[0].selectedSubtitle
|
||||
selectedSubtitle = subtitle
|
||||
exit for
|
||||
end if
|
||||
end for
|
||||
|
||||
if isValid(selectedSubtitle)
|
||||
availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName)
|
||||
if availableSubtitleTrackIndex <> -1
|
||||
if not selectedSubtitle.IsEncoded
|
||||
m.top.globalCaptionMode = "On"
|
||||
m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
m.top.selectedSubtitle = videoContent[0].selectedSubtitle
|
||||
|
||||
m.top.observeField("selectedSubtitle", "onSubtitleChange")
|
||||
|
||||
if isValid(m.top.audioIndex)
|
||||
m.top.audioTrack = (m.top.audioIndex + 1).toStr()
|
||||
else
|
||||
|
@ -375,6 +504,11 @@ sub onNextEpisodeDataLoaded()
|
|||
m.checkedForNextEpisode = true
|
||||
|
||||
m.top.observeField("position", "onPositionChanged")
|
||||
|
||||
' If there is no next episode, disable next episode button
|
||||
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
|
||||
m.nextupbuttonseconds = 0
|
||||
end if
|
||||
end sub
|
||||
|
||||
'
|
||||
|
@ -489,7 +623,7 @@ sub onState(msg)
|
|||
m.top.backPressed = true
|
||||
else if m.top.state = "playing"
|
||||
|
||||
' Check if next episde is available
|
||||
' Check if next episode is available
|
||||
if isValid(m.top.showID)
|
||||
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
|
||||
m.getNextEpisodeTask.showID = m.top.showID
|
||||
|
@ -579,6 +713,25 @@ function stateAllowsOSD() as boolean
|
|||
return inArray(validStates, m.top.state)
|
||||
end function
|
||||
|
||||
|
||||
' availSubtitleTrackIdx: Returns Roku's index for requested subtitle track
|
||||
'
|
||||
' @param {string} tracknameToFind - TrackName for subtitle we're looking to match
|
||||
' @return {integer} indicating Roku's index for requested subtitle track. Returns -1 if not found
|
||||
function availSubtitleTrackIdx(tracknameToFind as string) as integer
|
||||
idx = 0
|
||||
for each availTrack in m.top.availableSubtitleTracks
|
||||
' The TrackName must contain the URL we supplied originally, though
|
||||
' Roku mangles the name a bit, so we check if the URL is a substring, rather
|
||||
' than strict equality
|
||||
if Instr(1, availTrack.TrackName, tracknameToFind)
|
||||
return idx
|
||||
end if
|
||||
idx = idx + 1
|
||||
end for
|
||||
return -1
|
||||
end function
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
|
||||
' Keypress handler while user is inside the chapter menu
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
<interface>
|
||||
<field id="backPressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="selectSubtitlePressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="selectAudioPressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="PlaySessionId" type="string" />
|
||||
<field id="Subtitles" type="array" />
|
||||
<field id="SelectedSubtitle" type="integer" value="-1" alwaysNotify="true" />
|
||||
<field id="SelectedSubtitle" type="integer" value="-2" alwaysNotify="true" />
|
||||
<field id="previousSubtitleWasEncoded" type="boolean" />
|
||||
<field id="container" type="string" />
|
||||
<field id="directPlaySupported" type="boolean" />
|
||||
<field id="systemOverlay" type="boolean" value="false" />
|
||||
|
@ -22,6 +24,7 @@
|
|||
<field id="videoId" type="string" />
|
||||
<field id="mediaSourceId" type="string" />
|
||||
<field id="fullSubtitleData" type="array" />
|
||||
<field id="fullAudioData" type="array" />
|
||||
<field id="audioIndex" type="integer" />
|
||||
<field id="allowCaptions" type="boolean" value="false" />
|
||||
</interface>
|
||||
|
|
5
default.nix
Normal file
5
default.nix
Normal file
|
@ -0,0 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
# nativeBuildInputs are usually what you want -- tools you need to run
|
||||
nativeBuildInputs = with pkgs.buildPackages; [ nodejs ];
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user