Merge remote-tracking branch 'upstream/unstable' into rename-files
This commit is contained in:
commit
323b147401
26
.github/release.yml
vendored
Normal file
26
.github/release.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
changelog:
|
||||||
|
categories:
|
||||||
|
- title: 🆕 New Features
|
||||||
|
labels:
|
||||||
|
- "new feature"
|
||||||
|
- title: 🔨 Updated Features
|
||||||
|
labels:
|
||||||
|
- "updated feature"
|
||||||
|
- title: ⚙️ New Settings
|
||||||
|
labels:
|
||||||
|
- "new setting"
|
||||||
|
- title: 🔧 Updated Settings
|
||||||
|
labels:
|
||||||
|
- "updated setting"
|
||||||
|
- title: 🐛 Bug Fixes
|
||||||
|
labels:
|
||||||
|
- "bug fix"
|
||||||
|
- title: 🧹 Code Cleanup
|
||||||
|
labels:
|
||||||
|
- "code cleanup"
|
||||||
|
- title: ⭐ Additional Updates
|
||||||
|
labels:
|
||||||
|
- "*"
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- dependencies
|
1
.github/workflows/auto-close-stale-pr.yml
vendored
1
.github/workflows/auto-close-stale-pr.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
2
.github/workflows/automations.yml
vendored
2
.github/workflows/automations.yml
vendored
|
@ -12,6 +12,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
project:
|
project:
|
||||||
|
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||||
name: Project board 📊
|
name: Project board 📊
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -23,6 +24,7 @@ jobs:
|
||||||
column: In progress
|
column: In progress
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
label:
|
label:
|
||||||
|
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||||
name: Labeling 🏷️
|
name: Labeling 🏷️
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
4
.github/workflows/build-dev.yml
vendored
4
.github/workflows/build-dev.yml
vendored
|
@ -12,8 +12,8 @@ jobs:
|
||||||
dev:
|
dev:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
33
.github/workflows/build-docs.yml
vendored
Normal file
33
.github/workflows/build-docs.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: build-docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- unstable
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository.
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
- name: Install NPM dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Build API docs
|
||||||
|
# TODO: fix jsdoc build errors then remove '|| true' from run command below
|
||||||
|
run: npm run docs || true
|
||||||
|
- name: Commit any changes back to the unstable branch
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5
|
||||||
|
with:
|
||||||
|
commit_message: Update API docs
|
||||||
|
# use jellyfin-bot to commit the changes instead of the default github-actions[bot]
|
||||||
|
commit_user_name: jellyfin-bot
|
||||||
|
commit_user_email: team@jellyfin.org
|
||||||
|
# use jellyfin-bot to author the changes instead of the default author of the merge commit
|
||||||
|
commit_author: jellyfin-bot <team@jellyfin.org>
|
8
.github/workflows/build-prod.yml
vendored
8
.github/workflows/build-prod.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master (the latest release)
|
- name: Checkout master (the latest release)
|
||||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
- name: Install jq to parse json
|
- name: Install jq to parse json
|
||||||
|
@ -33,7 +33,7 @@ jobs:
|
||||||
- name: Save old Makefile version
|
- name: Save old Makefile version
|
||||||
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
|
||||||
- name: Checkout PR branch
|
- name: Checkout PR branch
|
||||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
- name: Save new package.json version
|
- name: Save new package.json version
|
||||||
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
|
||||||
- name: package.json version must be updated
|
- name: package.json version must be updated
|
||||||
|
@ -61,8 +61,8 @@ jobs:
|
||||||
prod:
|
prod:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
43
.github/workflows/deploy-api-docs.yml
vendored
Normal file
43
.github/workflows/deploy-api-docs.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Simple workflow for deploying static content to GitHub Pages
|
||||||
|
name: deploy-api-docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["unstable"]
|
||||||
|
paths: ["docs/**"] # only run if the docs are updated
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: github.repository == 'jellyfin/jellyfin-roku'
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@f156874f8191504dae5b037505266ed5dda6c382 # v3
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2
|
||||||
|
with:
|
||||||
|
# Only upload the api docs folder
|
||||||
|
path: "docs/api"
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@9dbe3824824f8a1377b8e298bafde1a50ede43e5 # v2
|
6
.github/workflows/roku-analysis.yml
vendored
6
.github/workflows/roku-analysis.yml
vendored
|
@ -11,8 +11,8 @@ jobs:
|
||||||
static:
|
static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
@ -27,7 +27,7 @@ jobs:
|
||||||
if: env.BRANCH_NAME == 'master'
|
if: env.BRANCH_NAME == 'master'
|
||||||
run: npm run build-prod
|
run: npm run build-prod
|
||||||
- name: Use Java 17
|
- name: Use Java 17
|
||||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3
|
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
|
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -4,9 +4,10 @@
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"RokuCommunity.brightscript",
|
"RokuCommunity.brightscript",
|
||||||
|
"AliceBeckett.brightscriptcomment",
|
||||||
"redhat.vscode-xml",
|
"redhat.vscode-xml",
|
||||||
"davidanson.vscode-markdownlint"
|
"davidanson.vscode-markdownlint"
|
||||||
],
|
],
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||||
"unwantedRecommendations": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
|
|
34
.vscode/settings.json
vendored
34
.vscode/settings.json
vendored
|
@ -1,15 +1,21 @@
|
||||||
{
|
{
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.ts": "xml"
|
"*.ts": "xml"
|
||||||
},
|
},
|
||||||
"[xml]": {
|
"[xml]": {
|
||||||
"editor.defaultFormatter": "redhat.vscode-xml"
|
"editor.defaultFormatter": "redhat.vscode-xml"
|
||||||
},
|
},
|
||||||
"[markdown]": {
|
"[markdown]": {
|
||||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||||
},
|
},
|
||||||
"xml.format.maxLineWidth": 0,
|
"xml.format.maxLineWidth": 0,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"brightscript.output.hyperlinkFormat": "FilenameAndFunction",
|
"brightscript.output.hyperlinkFormat": "FilenameAndFunction",
|
||||||
"brightscript.bsdk": "node_modules/brighterscript"
|
"brightscript.bsdk": "node_modules/brighterscript",
|
||||||
}
|
"search.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"docs/api/**": true
|
||||||
|
},
|
||||||
|
"brightscriptcomment.addExtraAtStartAndEnd": false
|
||||||
|
}
|
||||||
|
|
126
Makefile
126
Makefile
|
@ -1,28 +1,114 @@
|
||||||
|
##########################################################################
|
||||||
#########################################################################
|
# Need curl and npm in your $PATH
|
||||||
# Makefile Usage:
|
# If you want to get_images, you'll also need convert from ImageMagick
|
||||||
#
|
|
||||||
# 1) Make sure that you have the curl command line executable in your path
|
|
||||||
# 2) Set the variable ROKU_DEV_TARGET in your environment to the IP
|
|
||||||
# address of your Roku box. (e.g. export ROKU_DEV_TARGET=192.168.1.1.
|
|
||||||
# Set in your this variable in your shell startup (e.g. .bashrc)
|
|
||||||
# 3) and set up the ROKU_DEV_PASSWORD environment variable, too
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
APPNAME = Jellyfin_Roku
|
VERSION := 1.6.6
|
||||||
VERSION = 1.6.6
|
|
||||||
|
|
||||||
ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
|
## usage
|
||||||
|
|
||||||
include app.mk
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "targets"
|
||||||
|
@echo " build-dev build development package"
|
||||||
|
@echo " build-prod build production package"
|
||||||
|
@echo " build-tests build tests package"
|
||||||
|
@echo " format format brighscripts"
|
||||||
|
@echo " lint lint code and documentation"
|
||||||
|
@echo " get_images update official jellyfin images"
|
||||||
|
@echo "targets needing ROKU_DEV_TARGET"
|
||||||
|
@echo " home press the home button on device"
|
||||||
|
@echo " launch launch installed"
|
||||||
|
@echo "targets needing ROKU_DEV_TARGET and ROKU_DEV_PASSWORD"
|
||||||
|
@echo " install install on device"
|
||||||
|
@echo " remove remove installed from device"
|
||||||
|
@echo " screenshot take a screenshot"
|
||||||
|
@echo " deploy lint, remove, install"
|
||||||
|
@echo "environment"
|
||||||
|
@echo " ROKU_DEV_TARGET with device's IP"
|
||||||
|
@echo " ROKU_DEV_PASSWORD with device's password"
|
||||||
|
|
||||||
dev:
|
## development
|
||||||
$(MAKE) BUILD='dev' package
|
|
||||||
|
|
||||||
beta:
|
BUILT_PKG := out/$(notdir $(CURDIR)).zip
|
||||||
$(MAKE) BUILD='beta' package
|
|
||||||
|
|
||||||
release:
|
node_modules/: package-lock.json; npm ci
|
||||||
$(MAKE) BUILD='release' package
|
|
||||||
|
|
||||||
deploy: prep_staging remove install
|
.PHONY: build-dev build-prod build-tests
|
||||||
|
.NOTPARALLEL: build-dev build-prod build-tests # output to the same file
|
||||||
|
build-dev: node_modules/; npm run build
|
||||||
|
build-prod: node_modules/; npm run build-prod
|
||||||
|
build-tests: node_modules/; npm run build-tests
|
||||||
|
|
||||||
|
# default to build-dev if file doesn't exist
|
||||||
|
$(BUILT_PKG):; $(MAKE) build-dev
|
||||||
|
|
||||||
|
.PHONY: format
|
||||||
|
format: node_modules/; npm run format
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: node_modules/; npm run lint
|
||||||
|
|
||||||
|
## roku box
|
||||||
|
|
||||||
|
CURL_CMD ?= curl --show-error
|
||||||
|
|
||||||
|
ifdef ROKU_DEV_TARGET
|
||||||
|
|
||||||
|
.PHONY: home launch
|
||||||
|
home:
|
||||||
|
$(CURL_CMD) -XPOST http://$(ROKU_DEV_TARGET):8060/keypress/home
|
||||||
|
sleep 2 # wait for device reaction
|
||||||
|
launch:
|
||||||
|
$(CURL_CMD) -XPOST http://$(ROKU_DEV_TARGET):8060/launch/dev
|
||||||
|
|
||||||
|
ifdef ROKU_DEV_PASSWORD
|
||||||
|
|
||||||
|
CURL_LOGGED_CMD := $(CURL_CMD) --user rokudev:$(ROKU_DEV_PASSWORD) --digest
|
||||||
|
|
||||||
|
EXTRACT_ERROR_CMD := grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[["
|
||||||
|
.PHONY: install remove
|
||||||
|
install: $(BUILT_PKG) home
|
||||||
|
$(CURL_LOGGED_CMD) -F "mysubmit=Install" -F "archive=@$<" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | $(EXTRACT_ERROR_CMD)
|
||||||
|
$(MAKE) launch
|
||||||
|
remove:
|
||||||
|
$(CURL_LOGGED_CMD) -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | $(EXTRACT_ERROR_CMD)
|
||||||
|
|
||||||
|
.PHONY: screenshot
|
||||||
|
screenshot:
|
||||||
|
$(CURL_LOGGED_CMD) -o screenshot.jpg "http://$(ROKU_DEV_TARGET)/pkgs/dev.jpg"
|
||||||
|
|
||||||
|
.PHONY: deploy
|
||||||
|
.NOTPARALLEL: deploy
|
||||||
|
deploy: lint remove install
|
||||||
|
|
||||||
|
endif # ROKU_DEV_PASSWORD
|
||||||
|
|
||||||
|
endif # ROKU_DEV_TARGET
|
||||||
|
|
||||||
|
## sync branding
|
||||||
|
|
||||||
|
CONVERT_CMD ?= convert -gravity center
|
||||||
|
CONVERT_BLUEBG_CMD := $(CONVERT_CMD) -background "\#000b25"
|
||||||
|
BANNER := images/banner-dark.svg
|
||||||
|
ICON := images/icon-transparent.svg
|
||||||
|
|
||||||
|
images/:; mkdir $@
|
||||||
|
|
||||||
|
.PHONY: redo # force rerun
|
||||||
|
$(BANNER) $(ICON): images/ redo
|
||||||
|
$(CURL_CMD) https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/$(@F) > $@
|
||||||
|
|
||||||
|
images/logo.png: $(BANNER); $(CONVERT_CMD) -background none -scale 1000x48 -extent 180x48 $< $@
|
||||||
|
images/channel-poster_fhd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 535x400 -extent 540x405 $< $@
|
||||||
|
images/channel-poster_hd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 275x205 -extent 336x210 $< $@
|
||||||
|
images/channel-poster_sd.png: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 182x135 -extent 246x140 $< $@
|
||||||
|
images/splash-screen_fhd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 540x540 -extent 1920x1080 $< $@
|
||||||
|
images/splash-screen_hd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 360x360 -extent 1280x720 $< $@
|
||||||
|
images/splash-screen_sd.jpg: $(BANNER); $(CONVERT_BLUEBG_CMD) -scale 240x240 -extent 720x480 $< $@
|
||||||
|
|
||||||
|
.PHONY: get_images
|
||||||
|
get_images: $(ICON)
|
||||||
|
get_images: images/logo.png
|
||||||
|
get_images: images/channel-poster_fhd.png images/channel-poster_hd.png images/channel-poster_sd.png
|
||||||
|
get_images: images/splash-screen_fhd.jpg images/splash-screen_hd.jpg images/splash-screen_sd.jpg
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
[![Logo Banner](https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true "Jellyfin")](https://jellyfin.org)
|
[![Logo Banner](https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true "Jellyfin")](https://jellyfin.org)
|
||||||
|
|
||||||
|
[![Code Documentation](https://img.shields.io/badge/Code%20Documentation-purple)](https://jellyfin.github.io/jellyfin-roku/)
|
||||||
[![Build Status](https://img.shields.io/github/actions/workflow/status/jellyfin/jellyfin-roku/build-dev.yml?logo=github&branch=unstable "Build Status")](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable)
|
[![Build Status](https://img.shields.io/github/actions/workflow/status/jellyfin/jellyfin-roku/build-dev.yml?logo=github&branch=unstable "Build Status")](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable)
|
||||||
[![Current Release](https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg?logo=github "Current Release")](https://github.com/jellyfin/jellyfin-roku/releases)
|
[![Current Release](https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg?logo=github "Current Release")](https://github.com/jellyfin/jellyfin-roku/releases)
|
||||||
[![Translation Status](https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg "Translation Status")](https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget)
|
[![Translation Status](https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg "Translation Status")](https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget)
|
||||||
|
|
215
app.mk
215
app.mk
|
@ -1,215 +0,0 @@
|
||||||
#########################################################################
|
|
||||||
# common include file for application Makefiles
|
|
||||||
#
|
|
||||||
# Makefile Common Usage:
|
|
||||||
# > make
|
|
||||||
# > make install
|
|
||||||
# > make remove
|
|
||||||
#
|
|
||||||
# By default, ZIP_EXCLUDE will exclude -x \*.pkg -x storeassets\* -x keys\* -x .\*
|
|
||||||
# If you define ZIP_EXCLUDE in your Makefile, it will override the default setting.
|
|
||||||
#
|
|
||||||
# To exclude different files from being added to the zipfile during packaging
|
|
||||||
# include a line like this:ZIP_EXCLUDE= -x keys\*
|
|
||||||
# that will exclude any file who's name begins with 'keys'
|
|
||||||
# to exclude using more than one pattern use additional '-x <pattern>' arguments
|
|
||||||
# ZIP_EXCLUDE= -x \*.pkg -x storeassets\*
|
|
||||||
#
|
|
||||||
# Important Notes:
|
|
||||||
# To use the "install" and "remove" targets to install your
|
|
||||||
# application directly from the shell, you must do the following:
|
|
||||||
#
|
|
||||||
# 1) Make sure that you have the curl command line executable in your path
|
|
||||||
# 2) Set the variable ROKU_DEV_TARGET in your environment to the IP
|
|
||||||
# address of your Roku box. (e.g. export ROKU_DEV_TARGET=192.168.1.1.
|
|
||||||
# Set in your this variable in your shell startup (e.g. .bashrc)
|
|
||||||
# 3) Set the variable ROKU_DEV_PASSWORD in your environment for the password
|
|
||||||
# associated with the rokudev account.
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
BUILD = dev
|
|
||||||
|
|
||||||
DISTREL = $(shell pwd)/out
|
|
||||||
COMMONREL ?= $(shell pwd)/common
|
|
||||||
SOURCEREL = $(shell pwd)
|
|
||||||
|
|
||||||
ZIPREL = $(DISTREL)/apps
|
|
||||||
STAGINGREL = $(DISTREL)/staging
|
|
||||||
PKGREL = $(DISTREL)/packages
|
|
||||||
|
|
||||||
APPSOURCEDIR = source
|
|
||||||
IMPORTFILES = $(foreach f,$(IMPORTS),$(COMMONREL)/$f.brs)
|
|
||||||
IMPORTCLEANUP = $(foreach f,$(IMPORTS),$(APPSOURCEDIR)/$f.brs)
|
|
||||||
|
|
||||||
GITCOMMIT = $(shell git rev-parse --short HEAD)
|
|
||||||
BUILDDATE = $(shell date -u | awk '{ print $$2,$$3,$$6,$$4 }')
|
|
||||||
|
|
||||||
BRANDING_ROOT = https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG
|
|
||||||
ICON_SOURCE = icon-transparent.svg
|
|
||||||
BANNER_SOURCE = banner-dark.svg
|
|
||||||
OUTPUT_DIR = ./images
|
|
||||||
|
|
||||||
# Locales supported by Roku
|
|
||||||
SUPPORTED_LOCALES = en_US en_GB fr_CA es_ES de_DE it_IT pt_BR
|
|
||||||
|
|
||||||
ifdef ROKU_DEV_PASSWORD
|
|
||||||
USERPASS = rokudev:$(ROKU_DEV_PASSWORD)
|
|
||||||
else
|
|
||||||
USERPASS = rokudev
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef ZIP_EXCLUDE
|
|
||||||
ZIP_EXCLUDE= -x \*.pkg -x storeassets\* -x keys\* -x \*/.\*
|
|
||||||
endif
|
|
||||||
|
|
||||||
HTTPSTATUS = $(shell curl --silent --write-out "\n%{http_code}\n" $(ROKU_DEV_TARGET))
|
|
||||||
|
|
||||||
ifeq "$(HTTPSTATUS)" " 401"
|
|
||||||
CURLCMD = curl -S --tcp-fastopen --connect-timeout 2 --max-time 30 --retry 5
|
|
||||||
else
|
|
||||||
CURLCMD = curl -S --tcp-fastopen --connect-timeout 2 --max-time 30 --retry 5 --user $(USERPASS) --digest
|
|
||||||
endif
|
|
||||||
|
|
||||||
home:
|
|
||||||
@echo "Forcing roku to main menu screen $(ROKU_DEV_TARGET)..."
|
|
||||||
curl -s -S -d '' http://$(ROKU_DEV_TARGET):8060/keypress/home
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
prep_staging:
|
|
||||||
@echo "*** Preparing Staging Area ***"
|
|
||||||
@echo " >> removing old application zip $(ZIPREL)/$(APPNAME).zip"
|
|
||||||
@if [ -e "$(ZIPREL)/$(APPNAME).zip" ]; \
|
|
||||||
then \
|
|
||||||
rm $(ZIPREL)/$(APPNAME).zip; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
@echo " >> creating destination directory $(ZIPREL)"
|
|
||||||
@if [ ! -d $(ZIPREL) ]; \
|
|
||||||
then \
|
|
||||||
mkdir -p $(ZIPREL); \
|
|
||||||
fi
|
|
||||||
|
|
||||||
@echo " >> setting directory permissions for $(ZIPREL)"
|
|
||||||
@if [ ! -w $(ZIPREL) ]; \
|
|
||||||
then \
|
|
||||||
chmod 755 $(ZIPREL); \
|
|
||||||
fi
|
|
||||||
|
|
||||||
@echo " >> creating destination directory $(STAGINGREL)"
|
|
||||||
@if [ -d $(STAGINGREL) ]; \
|
|
||||||
then \
|
|
||||||
find $(STAGINGREL) -delete; \
|
|
||||||
fi; \
|
|
||||||
mkdir -p $(STAGINGREL); \
|
|
||||||
chmod -R 755 $(STAGINGREL); \
|
|
||||||
|
|
||||||
echo " >> moving application to $(STAGINGREL)"
|
|
||||||
cp $(SOURCEREL)/manifest $(STAGINGREL)/manifest
|
|
||||||
cp -r $(SOURCEREL)/source $(STAGINGREL)
|
|
||||||
cp -r $(SOURCEREL)/components $(STAGINGREL)
|
|
||||||
cp -r $(SOURCEREL)/images $(STAGINGREL)
|
|
||||||
cp -r $(SOURCEREL)/settings $(STAGINGREL)
|
|
||||||
|
|
||||||
# Copy only supported languages over to staging
|
|
||||||
mkdir $(STAGINGREL)/locale
|
|
||||||
cp -r $(foreach f,$(SUPPORTED_LOCALES),$(SOURCEREL)/locale/$f) $(STAGINGREL)/locale
|
|
||||||
|
|
||||||
ifneq ($(BUILD), dev)
|
|
||||||
echo "COPYING $(BUILD)"
|
|
||||||
cp $(SOURCEREL)/resources/branding/$(BUILD)/* $(STAGINGREL)/images
|
|
||||||
endif
|
|
||||||
|
|
||||||
package: prep_staging
|
|
||||||
@echo "*** Creating $(APPNAME).zip ***"
|
|
||||||
@echo " >> copying imports"
|
|
||||||
@if [ "$(IMPORTFILES)" ]; \
|
|
||||||
then \
|
|
||||||
mkdir $(APPSOURCEDIR)/common; \
|
|
||||||
cp -f -p -v $(IMPORTFILES) $(APPSOURCEDIR)/common/; \
|
|
||||||
fi \
|
|
||||||
|
|
||||||
@echo " >> generating build info file"
|
|
||||||
mkdir -p $(STAGINGREL)/$(APPSOURCEDIR)
|
|
||||||
@if [ -e "$(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs" ]; \
|
|
||||||
then \
|
|
||||||
rm $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs; \
|
|
||||||
fi
|
|
||||||
echo " >> generating build info file";\
|
|
||||||
echo "Function BuildDate()" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
echo " return \"${BUILDDATE}\"" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
echo "End Function" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
echo "Function BuildCommit()" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
echo " return \"${GITCOMMIT}\"" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
echo "End Function" >> $(STAGINGREL)/$(APPSOURCEDIR)/buildinfo.brs
|
|
||||||
|
|
||||||
# zip .png files without compression
|
|
||||||
# do not zip up any files ending with '~'
|
|
||||||
@echo " >> creating application zip $(STAGINGREL)/../apps/$(APPNAME)-$(BUILD).zip"
|
|
||||||
@if [ -d $(STAGINGREL) ]; \
|
|
||||||
then \
|
|
||||||
cd $(STAGINGREL); \
|
|
||||||
(zip -0 -r "../apps/$(APPNAME)-$(BUILD).zip" . -i \*.png $(ZIP_EXCLUDE)); \
|
|
||||||
(zip -9 -r "../apps/$(APPNAME)-$(BUILD).zip" . -x \*~ -x \*.png $(ZIP_EXCLUDE)); \
|
|
||||||
cd $(SOURCEREL);\
|
|
||||||
else \
|
|
||||||
echo "Source for $(APPNAME) not found at $(STAGINGREL)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
@if [ "$(IMPORTCLEANUP)" ]; \
|
|
||||||
then \
|
|
||||||
echo " >> deleting imports";\
|
|
||||||
rm -r -f $(APPSOURCEDIR)/common; \
|
|
||||||
fi \
|
|
||||||
|
|
||||||
@echo "*** packaging $(APPNAME)-$(BUILD) complete ***"
|
|
||||||
|
|
||||||
prep_commit:
|
|
||||||
npm run format
|
|
||||||
npm ci
|
|
||||||
npm run validate
|
|
||||||
npm run check-formatting
|
|
||||||
|
|
||||||
install: prep_staging package home
|
|
||||||
@echo "Installing $(APPNAME)-$(BUILD) to host $(ROKU_DEV_TARGET)"
|
|
||||||
@$(CURLCMD) --user $(USERPASS) --digest -F "mysubmit=Install" -F "archive=@$(ZIPREL)/$(APPNAME)-$(BUILD).zip" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[["
|
|
||||||
|
|
||||||
remove:
|
|
||||||
@echo "Removing $(APPNAME) from host $(ROKU_DEV_TARGET)"
|
|
||||||
@if [ "$(HTTPSTATUS)" == " 401" ]; \
|
|
||||||
then \
|
|
||||||
$(CURLCMD) --user $(USERPASS) --digest -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[[" ; \
|
|
||||||
else \
|
|
||||||
curl -s -S -F "mysubmit=Delete" -F "archive=" -F "passwd=" http://$(ROKU_DEV_TARGET)/plugin_install | grep "<font color" | sed "s/<font color=\"red\">//" | sed "s[</font>[[" ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
get_images:
|
|
||||||
@if [ ! -d $(OUTPUT_DIR) ]; \
|
|
||||||
then \
|
|
||||||
mkdir -p $(OUTPUT_DIR); \
|
|
||||||
echo "Creating images folder"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Downloading SVG source files from $(BRANDING_ROOT)"
|
|
||||||
@wget $(BRANDING_ROOT)/$(ICON_SOURCE) > /dev/null
|
|
||||||
@wget $(BRANDING_ROOT)/$(BANNER_SOURCE) > /dev/null
|
|
||||||
echo "Finished downloading SVG files"
|
|
||||||
|
|
||||||
echo "Creating image files"
|
|
||||||
@convert -background "#000b25" -gravity center -scale 535x400 -extent 540x405 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_fhd.png
|
|
||||||
@convert -background "#000b25" -gravity center -scale 275x205 -extent 336x210 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_hd.png
|
|
||||||
@convert -background "#000b25" -gravity center -scale 182x135 -extent 246x140 $(BANNER_SOURCE) $(OUTPUT_DIR)/channel-poster_sd.png
|
|
||||||
|
|
||||||
@convert -background none -gravity center -scale 1000x48 -extent 180x48 $(BANNER_SOURCE) $(OUTPUT_DIR)/logo.png
|
|
||||||
|
|
||||||
@convert -background "#000b25" -gravity center -scale 540x540 -extent 1920x1080 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_fhd.jpg
|
|
||||||
@convert -background "#000b25" -gravity center -scale 360x360 -extent 1280x720 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_hd.jpg
|
|
||||||
@convert -background "#000b25" -gravity center -scale 240x240 -extent 720x480 $(BANNER_SOURCE) $(OUTPUT_DIR)/splash-screen_sd.jpg
|
|
||||||
echo "Finished creating image files"
|
|
||||||
|
|
||||||
screenshot:
|
|
||||||
SCREENSHOT_TIME=`date "+%s"`; \
|
|
||||||
curl -m 1 -o screenshot.jpg --user $(USERPASS) --digest "http://$(ROKU_DEV_TARGET)/pkgs/dev.jpg?time=$$SCREENSHOT_TIME" -H 'Accept: image/png,image/*;q=0.8,*/*;q=0.5' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
"diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"],
|
||||||
"autoImportComponentScript": true,
|
"autoImportComponentScript": true,
|
||||||
"allowBrighterScriptInBrightScript": true,
|
"allowBrighterScriptInBrightScript": true,
|
||||||
"createPackage": false,
|
|
||||||
"stagingFolderPath": "build",
|
"stagingFolderPath": "build",
|
||||||
"plugins": ["rooibos-roku"],
|
"plugins": ["rooibos-roku"],
|
||||||
"rooibos": {
|
"rooibos": {
|
||||||
|
|
44
components/ButtonGroupVert.bs
Normal file
44
components/ButtonGroupVert.bs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
sub init()
|
||||||
|
m.top.layoutDirection = "vert"
|
||||||
|
m.top.observeField("focusedChild", "onFocusChanged")
|
||||||
|
m.top.observeField("focusButton", "onFocusButtonChanged")
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onFocusChanged()
|
||||||
|
if m.top.hasFocus()
|
||||||
|
m.top.getChild(0).setFocus(true)
|
||||||
|
m.top.focusButton = 0
|
||||||
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onFocusButtonChanged()
|
||||||
|
m.top.getChild(m.top.focusButton).setFocus(true)
|
||||||
|
end sub
|
||||||
|
|
||||||
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
|
if key = "OK"
|
||||||
|
m.top.selected = m.top.focusButton
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
if not press then return false
|
||||||
|
|
||||||
|
if key = "down"
|
||||||
|
i = m.top.focusButton
|
||||||
|
target = i + 1
|
||||||
|
if target >= m.top.getChildCount() then return false
|
||||||
|
m.top.focusButton = target
|
||||||
|
return true
|
||||||
|
else if key = "up"
|
||||||
|
i = m.top.focusButton
|
||||||
|
target = i - 1
|
||||||
|
if target < 0 then return false
|
||||||
|
m.top.focusButton = target
|
||||||
|
return true
|
||||||
|
else if key = "left" or key = "right"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
return false
|
||||||
|
end function
|
7
components/ButtonGroupVert.xml
Normal file
7
components/ButtonGroupVert.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<component name="ButtonGroupVert" extends="ButtonGroup">
|
||||||
|
<interface>
|
||||||
|
<field id="escape" type="string" alwaysNotify="true" />
|
||||||
|
<field id="selected" type="integer" alwaysNotify="true" />
|
||||||
|
</interface>
|
||||||
|
</component>
|
100
components/Buttons/SlideOutButton.bs
Normal file
100
components/Buttons/SlideOutButton.bs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
sub init()
|
||||||
|
m.buttonBackground = m.top.findNode("buttonBackground")
|
||||||
|
m.buttonIcon = m.top.findNode("buttonIcon")
|
||||||
|
m.buttonText = m.top.findNode("buttonText")
|
||||||
|
|
||||||
|
m.buttonText.visible = false
|
||||||
|
|
||||||
|
m.originalWidth = 0
|
||||||
|
|
||||||
|
m.top.observeField("background", "onBackgroundChanged")
|
||||||
|
m.top.observeField("icon", "onIconChanged")
|
||||||
|
m.top.observeField("text", "onTextChanged")
|
||||||
|
m.top.observeField("height", "onHeightChanged")
|
||||||
|
m.top.observeField("width", "onWidthChanged")
|
||||||
|
m.top.observeField("padding", "onPaddingChanged")
|
||||||
|
m.top.observeField("focusedChild", "onFocusChanged")
|
||||||
|
|
||||||
|
m.top.observeField("highlighted", "onHighlightChanged")
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onFocusChanged()
|
||||||
|
if m.top.hasFocus()
|
||||||
|
m.buttonText.visible = true
|
||||||
|
m.buttonBackground.blendColor = m.top.focusBackground
|
||||||
|
m.top.width = 250
|
||||||
|
else
|
||||||
|
m.buttonText.visible = false
|
||||||
|
m.top.width = m.originalWidth
|
||||||
|
onHighlightChanged()
|
||||||
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onHighlightChanged()
|
||||||
|
if m.top.highlighted
|
||||||
|
m.buttonBackground.blendColor = m.top.highlightBackground
|
||||||
|
else
|
||||||
|
m.buttonBackground.blendColor = m.top.background
|
||||||
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onBackgroundChanged()
|
||||||
|
m.buttonBackground.blendColor = m.top.background
|
||||||
|
m.top.unobserveField("background")
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onIconChanged()
|
||||||
|
m.buttonIcon.uri = m.top.icon
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onTextChanged()
|
||||||
|
m.buttonText.text = m.top.text
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub setIconSize()
|
||||||
|
height = m.buttonBackground.height
|
||||||
|
width = m.buttonBackground.width
|
||||||
|
if height > 0 and width > 0
|
||||||
|
' TODO: Use smallest number between them
|
||||||
|
m.buttonIcon.height = m.top.height
|
||||||
|
|
||||||
|
if m.top.padding > 0
|
||||||
|
m.buttonIcon.height = m.buttonIcon.height - m.top.padding
|
||||||
|
end if
|
||||||
|
|
||||||
|
m.buttonIcon.width = m.buttonIcon.height
|
||||||
|
|
||||||
|
m.buttonIcon.translation = [m.top.padding, ((height - m.buttonIcon.height) / 2)]
|
||||||
|
m.buttonText.translation = [m.top.padding + m.buttonIcon.width + 10, 12]
|
||||||
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onHeightChanged()
|
||||||
|
m.buttonBackground.height = m.top.height
|
||||||
|
setIconSize()
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onWidthChanged()
|
||||||
|
if m.originalWidth = 0
|
||||||
|
m.originalWidth = m.top.width
|
||||||
|
end if
|
||||||
|
|
||||||
|
m.buttonBackground.width = m.top.width
|
||||||
|
setIconSize()
|
||||||
|
end sub
|
||||||
|
|
||||||
|
sub onPaddingChanged()
|
||||||
|
setIconSize()
|
||||||
|
end sub
|
||||||
|
|
||||||
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
|
if not press then return false
|
||||||
|
|
||||||
|
if key = "OK" and m.top.hasFocus()
|
||||||
|
' Simply toggle the selected field to trigger the next event
|
||||||
|
m.top.selected = not m.top.selected
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
return false
|
||||||
|
end function
|
20
components/Buttons/SlideOutButton.xml
Normal file
20
components/Buttons/SlideOutButton.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<component name="SlideOutButton" extends="Group">
|
||||||
|
<children>
|
||||||
|
<Poster id="buttonBackground" uri="pkg:/images/white.9.png" />
|
||||||
|
<Poster id="buttonIcon" />
|
||||||
|
<Label id="buttonText" color="#ffffff" font="font:SmallestSystemFont" horizAlign="center" />
|
||||||
|
</children>
|
||||||
|
<interface>
|
||||||
|
<field id="background" type="color" value="" />
|
||||||
|
<field id="focusBackground" type="color" value="" />
|
||||||
|
<field id="highlightBackground" type="color" value="" />
|
||||||
|
<field id="text" type="string" value="" />
|
||||||
|
<field id="padding" type="integer" value="-1" />
|
||||||
|
<field id="height" type="integer" value="" />
|
||||||
|
<field id="width" type="integer" value="" />
|
||||||
|
<field id="icon" type="string" value="" />
|
||||||
|
<field id="selected" type="boolean" value="false" />
|
||||||
|
<field id="highlighted" type="boolean" value="false" />
|
||||||
|
</interface>
|
||||||
|
</component>
|
|
@ -35,7 +35,7 @@ end function
|
||||||
' Returns an array of playback info to be displayed during playback.
|
' Returns an array of playback info to be displayed during playback.
|
||||||
' In the future, with a custom playback info view, we can return an associated array.
|
' In the future, with a custom playback info view, we can return an associated array.
|
||||||
sub getPlaybackInfoTask()
|
sub getPlaybackInfoTask()
|
||||||
sessions = api.sessions.Get()
|
sessions = api.sessions.Get({ "deviceId": m.global.device.serverDeviceName })
|
||||||
|
|
||||||
m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
|
m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,15 @@ sub loadInitialItems()
|
||||||
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
m.view = m.global.session.user.settings["display." + m.top.parentItem.Id + ".landing"]
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if m.sortField = invalid then m.sortField = "SortName"
|
if m.sortField = invalid
|
||||||
|
' Set the default order for boxsets to the Release Date - API calls it PremiereDate
|
||||||
|
if LCase(m.top.parentItem.json.Type) = "boxset"
|
||||||
|
m.sortField = "PremiereDate"
|
||||||
|
else
|
||||||
|
m.sortField = "SortName"
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
if m.filter = invalid then m.filter = "All"
|
if m.filter = invalid then m.filter = "All"
|
||||||
|
|
||||||
if sortAscendingStr = invalid or sortAscendingStr = true
|
if sortAscendingStr = invalid or sortAscendingStr = true
|
||||||
|
@ -717,6 +725,7 @@ sub showTVGuide()
|
||||||
m.tvGuide.filter = m.filter
|
m.tvGuide.filter = m.filter
|
||||||
m.tvGuide.searchTerm = m.voiceBox.text
|
m.tvGuide.searchTerm = m.voiceBox.text
|
||||||
m.top.appendChild(m.tvGuide)
|
m.top.appendChild(m.tvGuide)
|
||||||
|
m.scheduleGrid = m.top.findNode("scheduleGrid")
|
||||||
m.tvGuide.lastFocus.setFocus(true)
|
m.tvGuide.lastFocus.setFocus(true)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
@ -734,6 +743,18 @@ sub onChannelFocused(msg)
|
||||||
m.channelFocused = node.focusedChannel
|
m.channelFocused = node.focusedChannel
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
'Returns Focused Item
|
||||||
|
function getItemFocused()
|
||||||
|
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||||
|
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||||
|
else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused)
|
||||||
|
return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1])
|
||||||
|
else if m.scheduleGrid.isinFocusChain() and isValid(m.scheduleGrid.itemFocused)
|
||||||
|
return m.scheduleGrid.content.getChild(m.scheduleGrid.itemFocused)
|
||||||
|
end if
|
||||||
|
return invalid
|
||||||
|
end function
|
||||||
|
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
if not press then return false
|
if not press then return false
|
||||||
|
|
||||||
|
@ -780,11 +801,11 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.loadItemsTask.control = "stop"
|
m.loadItemsTask.control = "stop"
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
else if key = "play" or key = "OK"
|
else if key = "play"
|
||||||
markupGrid = m.top.findNode("itemGrid")
|
markupGrid = m.top.findNode("itemGrid")
|
||||||
itemToPlay = markupGrid.content.getChild(markupGrid.itemFocused)
|
itemToPlay = getItemFocused()
|
||||||
|
|
||||||
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
if itemToPlay <> invalid
|
||||||
m.top.quickPlayNode = itemToPlay
|
m.top.quickPlayNode = itemToPlay
|
||||||
return true
|
return true
|
||||||
else if itemToPlay <> invalid and itemToPlay.type = "Photo"
|
else if itemToPlay <> invalid and itemToPlay.type = "Photo"
|
||||||
|
|
|
@ -141,7 +141,6 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
||||||
|
|
||||||
|
|
||||||
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
||||||
|
|
||||||
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
||||||
fully_external = false
|
fully_external = false
|
||||||
|
|
||||||
|
@ -164,8 +163,8 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if video.directPlaySupported
|
if video.directPlaySupported
|
||||||
addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|
||||||
video.isTranscoded = false
|
video.isTranscoded = false
|
||||||
|
addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||||
else
|
else
|
||||||
if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid
|
if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid
|
||||||
' If server does not provide a transcode URL, display a message to the user
|
' If server does not provide a transcode URL, display a message to the user
|
||||||
|
@ -203,15 +202,13 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
||||||
fully_external = true
|
fully_external = true
|
||||||
video.content.url = m.playbackInfo.MediaSources[0].Path
|
video.content.url = m.playbackInfo.MediaSources[0].Path
|
||||||
end if
|
end if
|
||||||
else:
|
else
|
||||||
params = {}
|
params = {
|
||||||
|
|
||||||
params.append({
|
|
||||||
"Static": "true",
|
"Static": "true",
|
||||||
"Container": video.container,
|
"Container": video.container,
|
||||||
"PlaySessionId": video.PlaySessionId,
|
"PlaySessionId": video.PlaySessionId,
|
||||||
"AudioStreamIndex": audio_stream_idx
|
"AudioStreamIndex": audio_stream_idx
|
||||||
})
|
}
|
||||||
|
|
||||||
if mediaSourceId <> ""
|
if mediaSourceId <> ""
|
||||||
params.MediaSourceId = mediaSourceId
|
params.MediaSourceId = mediaSourceId
|
||||||
|
|
|
@ -144,6 +144,7 @@ sub loadInitialItems()
|
||||||
if not isValid(m.filter) then m.filter = "All"
|
if not isValid(m.filter) then m.filter = "All"
|
||||||
if not isValid(m.filterOptions) then m.filterOptions = "{}"
|
if not isValid(m.filterOptions) then m.filterOptions = "{}"
|
||||||
if not isValid(m.view) then m.view = "Movies"
|
if not isValid(m.view) then m.view = "Movies"
|
||||||
|
if not isValid(m.sortAscending) then m.sortAscending = true
|
||||||
|
|
||||||
m.filterOptions = ParseJson(m.filterOptions)
|
m.filterOptions = ParseJson(m.filterOptions)
|
||||||
|
|
||||||
|
@ -707,7 +708,12 @@ end sub
|
||||||
'
|
'
|
||||||
'Returns Focused Item
|
'Returns Focused Item
|
||||||
function getItemFocused()
|
function getItemFocused()
|
||||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||||
|
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||||
|
else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused)
|
||||||
|
return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1])
|
||||||
|
end if
|
||||||
|
return invalid
|
||||||
end function
|
end function
|
||||||
|
|
||||||
'
|
'
|
||||||
|
@ -869,11 +875,10 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.loadItemsTask.control = "stop"
|
m.loadItemsTask.control = "stop"
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
else if key = "play" or key = "OK"
|
else if key = "play"
|
||||||
|
|
||||||
itemToPlay = getItemFocused()
|
itemToPlay = getItemFocused()
|
||||||
|
|
||||||
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
if itemToPlay <> invalid
|
||||||
m.top.quickPlayNode = itemToPlay
|
m.top.quickPlayNode = itemToPlay
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
|
@ -131,6 +131,7 @@ sub loadInitialItems()
|
||||||
if not isValid(m.sortField) then m.sortField = "SortName"
|
if not isValid(m.sortField) then m.sortField = "SortName"
|
||||||
if not isValid(m.filter) then m.filter = "All"
|
if not isValid(m.filter) then m.filter = "All"
|
||||||
if not isValid(m.view) then m.view = "ArtistsPresentation"
|
if not isValid(m.view) then m.view = "ArtistsPresentation"
|
||||||
|
if not isValid(m.sortAscending) then m.sortAscending = true
|
||||||
|
|
||||||
m.top.showItemTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
m.top.showItemTitles = m.global.session.user.settings["itemgrid.gridTitles"]
|
||||||
|
|
||||||
|
@ -572,7 +573,12 @@ end sub
|
||||||
'
|
'
|
||||||
'Returns Focused Item
|
'Returns Focused Item
|
||||||
function getItemFocused()
|
function getItemFocused()
|
||||||
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused)
|
||||||
|
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
|
||||||
|
else if m.genreList.isinFocusChain() and isValid(m.genreList.itemFocused)
|
||||||
|
return m.genreList.content.getChild(m.genreList.itemFocused)
|
||||||
|
end if
|
||||||
|
return invalid
|
||||||
end function
|
end function
|
||||||
|
|
||||||
'
|
'
|
||||||
|
@ -750,7 +756,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
alpha.setFocus(true)
|
alpha.setFocus(true)
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
||||||
else if key = "right" and m.Alpha.isinFocusChain()
|
else if key = "right" and m.Alpha.isinFocusChain()
|
||||||
m.top.alphaActive = false
|
m.top.alphaActive = false
|
||||||
m.Alpha.setFocus(false)
|
m.Alpha.setFocus(false)
|
||||||
|
@ -760,14 +765,12 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.genreList.setFocus(m.genreList.opacity = 1)
|
m.genreList.setFocus(m.genreList.opacity = 1)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
else if key = "replay" and m.itemGrid.isinFocusChain()
|
else if key = "replay" and m.itemGrid.isinFocusChain()
|
||||||
if m.resetGrid = true
|
if m.resetGrid = true
|
||||||
m.itemGrid.animateToItem = 0
|
m.itemGrid.animateToItem = 0
|
||||||
else
|
else
|
||||||
m.itemGrid.jumpToItem = 0
|
m.itemGrid.jumpToItem = 0
|
||||||
end if
|
end if
|
||||||
|
|
||||||
else if key = "replay" and m.genreList.isinFocusChain()
|
else if key = "replay" and m.genreList.isinFocusChain()
|
||||||
if m.resetGrid = true
|
if m.resetGrid = true
|
||||||
m.genreList.animateToItem = 0
|
m.genreList.animateToItem = 0
|
||||||
|
@ -775,6 +778,12 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.genreList.jumpToItem = 0
|
m.genreList.jumpToItem = 0
|
||||||
end if
|
end if
|
||||||
return true
|
return true
|
||||||
|
else if key = "play"
|
||||||
|
itemToPlay = getItemFocused()
|
||||||
|
if itemToPlay <> invalid
|
||||||
|
m.top.quickPlayNode = itemToPlay
|
||||||
|
return true
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if key = "replay"
|
if key = "replay"
|
||||||
|
|
|
@ -90,10 +90,13 @@ end sub
|
||||||
'
|
'
|
||||||
' Runs Next Episode button animation and sets focus to button
|
' Runs Next Episode button animation and sets focus to button
|
||||||
sub showNextEpisodeButton()
|
sub showNextEpisodeButton()
|
||||||
if m.global.session.user.configuration.EnableNextEpisodeAutoPlay and not m.nextEpisodeButton.visible
|
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||||
|
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||||
|
|
||||||
|
if m.nextEpisodeButton.opacity = 0 and m.global.session.user.configuration.EnableNextEpisodeAutoPlay
|
||||||
|
m.nextEpisodeButton.visible = true
|
||||||
m.showNextEpisodeButtonAnimation.control = "start"
|
m.showNextEpisodeButtonAnimation.control = "start"
|
||||||
m.nextEpisodeButton.setFocus(true)
|
m.nextEpisodeButton.setFocus(true)
|
||||||
m.nextEpisodeButton.visible = true
|
|
||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
@ -117,13 +120,22 @@ end sub
|
||||||
|
|
||||||
' Checks if we need to display the Next Episode button
|
' Checks if we need to display the Next Episode button
|
||||||
sub checkTimeToDisplayNextEpisode()
|
sub checkTimeToDisplayNextEpisode()
|
||||||
if m.top.content.contenttype <> 4 then return
|
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||||
if m.nextupbuttonseconds = 0 then return
|
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||||
|
|
||||||
if int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds)
|
if isValid(m.top.duration) and isValid(m.top.position)
|
||||||
showNextEpisodeButton()
|
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
|
||||||
updateCount()
|
|
||||||
return
|
if nextEpisodeCountdown < 0 and m.nextEpisodeButton.opacity = 0.9
|
||||||
|
hideNextEpisodeButton()
|
||||||
|
return
|
||||||
|
else if nextEpisodeCountdown > 1 and int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds - 1)
|
||||||
|
updateCount()
|
||||||
|
if m.nextEpisodeButton.opacity = 0
|
||||||
|
showNextEpisodeButton()
|
||||||
|
end if
|
||||||
|
return
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||||
|
@ -259,6 +271,20 @@ sub bufferCheck(msg)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
|
|
||||||
|
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
|
||||||
|
m.top.state = "finished"
|
||||||
|
hideNextEpisodeButton()
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
'Hide Next Episode Button
|
||||||
|
if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus()
|
||||||
|
m.nextEpisodeButton.opacity = 0
|
||||||
|
m.nextEpisodeButton.setFocus(false)
|
||||||
|
m.top.setFocus(true)
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
if not press then return false
|
if not press then return false
|
||||||
|
|
||||||
if key = "down"
|
if key = "down"
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
||||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
||||||
</Animation>
|
</Animation>
|
||||||
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
|
<Animation id="hideNextEpisodeButton" duration=".25" repeat="false" easeFunction="inQuad">
|
||||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
||||||
</Animation>
|
</Animation>
|
||||||
</children>
|
</children>
|
||||||
|
|
|
@ -27,7 +27,7 @@ sub onItemSelected()
|
||||||
i = m.top.itemSelected
|
i = m.top.itemSelected
|
||||||
itemField = m.top.content.getchild(i)
|
itemField = m.top.content.getchild(i)
|
||||||
|
|
||||||
show_dialog(itemField)
|
configListShowDialog(itemField)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function onDialogButton()
|
function onDialogButton()
|
||||||
|
@ -46,7 +46,7 @@ function onDialogButton()
|
||||||
end function
|
end function
|
||||||
|
|
||||||
|
|
||||||
sub show_dialog(configField)
|
sub configListShowDialog(configField)
|
||||||
dialog = createObject("roSGNode", "StandardKeyboardDialog")
|
dialog = createObject("roSGNode", "StandardKeyboardDialog")
|
||||||
m.configField = configField
|
m.configField = configField
|
||||||
dialog.title = configField.label
|
dialog.title = configField.label
|
||||||
|
|
|
@ -31,7 +31,7 @@ sub setData()
|
||||||
m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png"
|
m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png"
|
||||||
end if
|
end if
|
||||||
|
|
||||||
else if datum.type = "Episode"
|
else if datum.type = "Episode" or datum.type = "MusicVideo"
|
||||||
m.top.isWatched = datum.UserData.Played
|
m.top.isWatched = datum.UserData.Played
|
||||||
|
|
||||||
imgParams = {}
|
imgParams = {}
|
||||||
|
@ -72,32 +72,7 @@ sub setData()
|
||||||
m.top.widePosterUrl = ImageURL(datum.Id, "Backdrop", imgParams)
|
m.top.widePosterUrl = ImageURL(datum.Id, "Backdrop", imgParams)
|
||||||
end if
|
end if
|
||||||
|
|
||||||
else if datum.type = "Movie"
|
else if datum.type = "Movie" or datum.type = "Video"
|
||||||
m.top.isWatched = datum.UserData.Played
|
|
||||||
|
|
||||||
imgParams = {}
|
|
||||||
imgParams.Append({ "maxHeight": 261 })
|
|
||||||
imgParams.Append({ "maxWidth": 175 })
|
|
||||||
|
|
||||||
if datum.ImageTags.Primary <> invalid
|
|
||||||
param = { "Tag": datum.ImageTags.Primary }
|
|
||||||
imgParams.Append(param)
|
|
||||||
end if
|
|
||||||
|
|
||||||
m.top.posterURL = ImageURL(datum.id, "Primary", imgParams)
|
|
||||||
|
|
||||||
' For wide image, use backdrop
|
|
||||||
imgParams["maxWidth"] = 464
|
|
||||||
|
|
||||||
if datum.ImageTags <> invalid and datum.imageTags.Thumb <> invalid
|
|
||||||
imgParams["Tag"] = datum.imageTags.Thumb
|
|
||||||
m.top.thumbnailUrl = ImageURL(datum.Id, "Thumb", imgParams)
|
|
||||||
else if datum.BackdropImageTags[0] <> invalid
|
|
||||||
imgParams["Tag"] = datum.BackdropImageTags[0]
|
|
||||||
m.top.thumbnailUrl = ImageURL(datum.id, "Backdrop", imgParams)
|
|
||||||
end if
|
|
||||||
|
|
||||||
else if datum.type = "Video"
|
|
||||||
m.top.isWatched = datum.UserData.Played
|
m.top.isWatched = datum.UserData.Played
|
||||||
|
|
||||||
imgParams = {}
|
imgParams = {}
|
||||||
|
@ -126,12 +101,10 @@ sub setData()
|
||||||
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
|
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
|
||||||
m.top.widePosterUrl = m.top.thumbnailURL
|
m.top.widePosterUrl = m.top.thumbnailURL
|
||||||
m.top.posterUrl = m.top.thumbnailURL
|
m.top.posterUrl = m.top.thumbnailURL
|
||||||
|
|
||||||
else if datum.type = "TvChannel" or datum.type = "Channel"
|
else if datum.type = "TvChannel" or datum.type = "Channel"
|
||||||
params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
|
params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
|
||||||
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
|
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
|
||||||
m.top.widePosterUrl = m.top.thumbnailURL
|
m.top.widePosterUrl = m.top.thumbnailURL
|
||||||
m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
|
m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
|
||||||
end if
|
end if
|
||||||
|
|
||||||
end sub
|
end sub
|
||||||
|
|
|
@ -77,17 +77,16 @@ end sub
|
||||||
sub popScene()
|
sub popScene()
|
||||||
group = m.groups.pop()
|
group = m.groups.pop()
|
||||||
if group <> invalid
|
if group <> invalid
|
||||||
groupType = group.subtype()
|
if group.isSubType("JFGroup")
|
||||||
if groupType = "JFGroup"
|
|
||||||
unregisterOverhangData(group)
|
unregisterOverhangData(group)
|
||||||
else if groupType = "JFVideo"
|
else if group.isSubType("JFVideo")
|
||||||
' Stop video to make sure app communicates stop playstate to server
|
' Stop video to make sure app communicates stop playstate to server
|
||||||
group.control = "stop"
|
group.control = "stop"
|
||||||
end if
|
end if
|
||||||
|
|
||||||
group.visible = false
|
group.visible = false
|
||||||
|
|
||||||
if groupType = "JFScreen"
|
if group.isSubType("JFScreen")
|
||||||
group.callFunc("OnScreenHidden")
|
group.callFunc("OnScreenHidden")
|
||||||
end if
|
end if
|
||||||
else
|
else
|
||||||
|
|
|
@ -3,6 +3,7 @@ sub init()
|
||||||
updateSize()
|
updateSize()
|
||||||
m.top.rowFocusAnimationStyle = "fixedFocus"
|
m.top.rowFocusAnimationStyle = "fixedFocus"
|
||||||
m.top.observeField("rowItemSelected", "onRowItemSelected")
|
m.top.observeField("rowItemSelected", "onRowItemSelected")
|
||||||
|
m.top.observeField("rowItemFocused", "onRowItemFocused")
|
||||||
|
|
||||||
' Set up all Tasks
|
' Set up all Tasks
|
||||||
m.LoadPeopleTask = CreateObject("roSGNode", "LoadItemsTask")
|
m.LoadPeopleTask = CreateObject("roSGNode", "LoadItemsTask")
|
||||||
|
@ -207,3 +208,7 @@ end sub
|
||||||
sub onRowItemSelected()
|
sub onRowItemSelected()
|
||||||
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
|
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
sub onRowItemFocused()
|
||||||
|
m.top.focusedItem = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
|
||||||
|
end sub
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<interface>
|
<interface>
|
||||||
<field id="type" type="string" />
|
<field id="type" type="string" />
|
||||||
<field id="parentId" type="string" />
|
<field id="parentId" type="string" />
|
||||||
|
<field id="focusedItem" type="node" alwaysNotify="true" />
|
||||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||||
<function name="loadParts" />
|
<function name="loadParts" />
|
||||||
<function name="loadPersonVideos" />
|
<function name="loadPersonVideos" />
|
||||||
|
|
|
@ -30,6 +30,7 @@ end sub
|
||||||
sub itemContentChanged()
|
sub itemContentChanged()
|
||||||
itemData = m.top.itemContent
|
itemData = m.top.itemContent
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem"
|
itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem"
|
||||||
|
|
||||||
m.itemPoster.width = itemData.imageWidth
|
m.itemPoster.width = itemData.imageWidth
|
||||||
|
@ -135,7 +136,7 @@ sub itemContentChanged()
|
||||||
return
|
return
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if itemData.type = "Movie"
|
if itemData.type = "Movie" or itemData.type = "MusicVideo"
|
||||||
m.itemText.text = itemData.name
|
m.itemText.text = itemData.name
|
||||||
|
|
||||||
if itemData.PlayedPercentage > 0
|
if itemData.PlayedPercentage > 0
|
||||||
|
|
|
@ -12,6 +12,10 @@ sub init()
|
||||||
m.top.rowLabelOffset = [0, 20]
|
m.top.rowLabelOffset = [0, 20]
|
||||||
m.top.showRowCounter = [true]
|
m.top.showRowCounter = [true]
|
||||||
|
|
||||||
|
m.homeSectionIndexes = {
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
|
||||||
updateSize()
|
updateSize()
|
||||||
|
|
||||||
m.top.setfocus(true)
|
m.top.setfocus(true)
|
||||||
|
@ -23,8 +27,8 @@ sub init()
|
||||||
m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
|
m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
|
||||||
|
|
||||||
' set up tesk nodes for other rows
|
' set up tesk nodes for other rows
|
||||||
m.LoadContinueTask = createObject("roSGNode", "LoadItemsTask")
|
m.LoadContinueWatchingTask = createObject("roSGNode", "LoadItemsTask")
|
||||||
m.LoadContinueTask.itemsToLoad = "continue"
|
m.LoadContinueWatchingTask.itemsToLoad = "continue"
|
||||||
|
|
||||||
m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
|
m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
|
||||||
m.LoadNextUpTask.itemsToLoad = "nextUp"
|
m.LoadNextUpTask.itemsToLoad = "nextUp"
|
||||||
|
@ -61,73 +65,199 @@ sub onLibrariesLoaded()
|
||||||
m.libraryData = m.LoadLibrariesTask.content
|
m.libraryData = m.LoadLibrariesTask.content
|
||||||
m.LoadLibrariesTask.unobserveField("content")
|
m.LoadLibrariesTask.unobserveField("content")
|
||||||
m.LoadLibrariesTask.content = []
|
m.LoadLibrariesTask.content = []
|
||||||
' create My Media, Continue Watching, and Next Up rows
|
|
||||||
content = CreateObject("roSGNode", "ContentNode")
|
content = CreateObject("roSGNode", "ContentNode")
|
||||||
|
sizeArray = []
|
||||||
|
loadedSections = 0
|
||||||
|
|
||||||
|
' Add sections in order based on user settings
|
||||||
|
for i = 0 to 6
|
||||||
|
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
||||||
|
sectionLoaded = addHomeSection(content, sizeArray, sectionName)
|
||||||
|
|
||||||
|
' Count how many sections with data are loaded
|
||||||
|
if sectionLoaded then loadedSections++
|
||||||
|
|
||||||
|
' If 2 sections with data are loaded or we're at the end of the web client section data, consider the home view loaded
|
||||||
|
if loadedSections = 2 or i = 6
|
||||||
|
if not m.global.app_loaded
|
||||||
|
m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
|
||||||
|
m.global.app_loaded = true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
end for
|
||||||
|
|
||||||
|
' Favorites isn't an option on Web settings, so we must manually add it for now
|
||||||
|
addHomeSection(content, sizeArray, "favorites")
|
||||||
|
|
||||||
|
m.top.rowItemSize = sizeArray
|
||||||
|
m.top.content = content
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Removes a home section from the home rows
|
||||||
|
sub removeHomeSection(sectionType as string)
|
||||||
|
sectionName = LCase(sectionType)
|
||||||
|
|
||||||
|
removedSectionIndex = m.homeSectionIndexes[sectionName]
|
||||||
|
|
||||||
|
if not isValid(removedSectionIndex) then return
|
||||||
|
|
||||||
|
for each section in m.homeSectionIndexes
|
||||||
|
if m.homeSectionIndexes[section] > removedSectionIndex
|
||||||
|
m.homeSectionIndexes[section]--
|
||||||
|
end if
|
||||||
|
end for
|
||||||
|
|
||||||
|
m.homeSectionIndexes.Delete(sectionName)
|
||||||
|
m.homeSectionIndexes.AddReplace("count", m.homeSectionIndexes.count - 1)
|
||||||
|
|
||||||
|
m.top.content.removeChildIndex(removedSectionIndex)
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Adds a new home section to the home rows.
|
||||||
|
' Returns a boolean indicating whether the section was handled.
|
||||||
|
function addHomeSection(content as dynamic, sizeArray as dynamic, sectionName as string) as boolean
|
||||||
|
' Poster size library items
|
||||||
|
if sectionName = "livetv"
|
||||||
|
createLiveTVRow(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
' Poster size library items
|
||||||
|
if sectionName = "smalllibrarytiles"
|
||||||
|
createLibraryRow(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
' Continue Watching items
|
||||||
|
if sectionName = "resume"
|
||||||
|
createContinueWatchingRow(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
' Next Up items
|
||||||
|
if sectionName = "nextup"
|
||||||
|
createNextUpRow(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
' Latest items in each library
|
||||||
|
if sectionName = "latestmedia"
|
||||||
|
createLatestInRows(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
' Favorite Items
|
||||||
|
if sectionName = "favorites"
|
||||||
|
createFavoritesRow(content, sizeArray)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
return false
|
||||||
|
end function
|
||||||
|
|
||||||
|
' Create a row displaying the user's libraries
|
||||||
|
sub createLibraryRow(content as dynamic, sizeArray as dynamic)
|
||||||
|
' Ensure we have data
|
||||||
|
if not isValidAndNotEmpty(m.libraryData) then return
|
||||||
|
|
||||||
mediaRow = content.CreateChild("HomeRow")
|
mediaRow = content.CreateChild("HomeRow")
|
||||||
mediaRow.title = tr("My Media")
|
mediaRow.title = tr("My Media")
|
||||||
|
|
||||||
continueRow = content.CreateChild("HomeRow")
|
m.homeSectionIndexes.AddReplace("library", m.homeSectionIndexes.count)
|
||||||
continueRow.title = tr("Continue Watching")
|
m.homeSectionIndexes.count++
|
||||||
|
|
||||||
nextUpRow = content.CreateChild("HomeRow")
|
sizeArray.push([464, 331])
|
||||||
nextUpRow.title = tr("Next Up >")
|
|
||||||
|
|
||||||
favoritesRow = content.CreateChild("HomeRow")
|
filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
|
||||||
favoritesRow.title = tr("Favorites")
|
for each item in filteredMedia
|
||||||
|
mediaRow.appendChild(item)
|
||||||
|
end for
|
||||||
|
end sub
|
||||||
|
|
||||||
sizeArray = [
|
' Create a row displaying latest items in each of the user's libraries
|
||||||
[464, 311], ' My Media
|
sub createLatestInRows(content as dynamic, sizeArray as dynamic)
|
||||||
[464, 331], ' Continue Watching
|
' Ensure we have data
|
||||||
[464, 331], ' Next Up
|
if not isValidAndNotEmpty(m.libraryData) then return
|
||||||
[464, 331] ' Favorites
|
|
||||||
]
|
|
||||||
|
|
||||||
haveLiveTV = false
|
' create a "Latest In" row for each library
|
||||||
|
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
|
||||||
|
for each lib in filteredLatest
|
||||||
|
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
|
||||||
|
latestInRow = content.CreateChild("HomeRow")
|
||||||
|
latestInRow.title = tr("Latest in") + " " + lib.name + " >"
|
||||||
|
|
||||||
' validate library data
|
m.homeSectionIndexes.AddReplace("latestin" + LCase(lib.name).Replace(" ", ""), m.homeSectionIndexes.count)
|
||||||
if isValid(m.libraryData) and m.libraryData.count() > 0
|
m.homeSectionIndexes.count++
|
||||||
' populate My Media row
|
sizeArray.Push([464, 331])
|
||||||
filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
|
|
||||||
for each item in filteredMedia
|
|
||||||
mediaRow.appendChild(item)
|
|
||||||
end for
|
|
||||||
|
|
||||||
' create a "Latest In" row for each library
|
loadLatest = createObject("roSGNode", "LoadItemsTask")
|
||||||
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
|
loadLatest.itemsToLoad = "latest"
|
||||||
for each lib in filteredLatest
|
loadLatest.itemId = lib.id
|
||||||
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
|
|
||||||
latestInRow = content.CreateChild("HomeRow")
|
|
||||||
latestInRow.title = tr("Latest in") + " " + lib.name + " >"
|
|
||||||
sizeArray.Push([464, 331])
|
|
||||||
else if lib.collectionType = "livetv"
|
|
||||||
' If we have Live TV, add "On Now"
|
|
||||||
onNowRow = content.CreateChild("HomeRow")
|
|
||||||
onNowRow.title = tr("On Now")
|
|
||||||
sizeArray.Push([464, 331])
|
|
||||||
haveLiveTV = true
|
|
||||||
end if
|
|
||||||
end for
|
|
||||||
end if
|
|
||||||
|
|
||||||
m.top.rowItemSize = sizeArray
|
metadata = { "title": lib.name }
|
||||||
m.top.content = content
|
metadata.Append({ "contentType": lib.json.CollectionType })
|
||||||
|
loadLatest.metadata = metadata
|
||||||
|
|
||||||
|
loadLatest.observeField("content", "updateLatestItems")
|
||||||
|
loadLatest.control = "RUN"
|
||||||
|
end if
|
||||||
|
end for
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Create a row displaying the live tv now on section
|
||||||
|
sub createLiveTVRow(content as dynamic, sizeArray as dynamic)
|
||||||
|
contentRow = content.CreateChild("HomeRow")
|
||||||
|
contentRow.title = tr("On Now")
|
||||||
|
m.homeSectionIndexes.AddReplace("livetv", m.homeSectionIndexes.count)
|
||||||
|
m.homeSectionIndexes.count++
|
||||||
|
sizeArray.push([464, 331])
|
||||||
|
|
||||||
|
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
||||||
|
m.LoadOnNowTask.control = "RUN"
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Create a row displaying items the user can continue watching
|
||||||
|
sub createContinueWatchingRow(content as dynamic, sizeArray as dynamic)
|
||||||
|
continueWatchingRow = content.CreateChild("HomeRow")
|
||||||
|
continueWatchingRow.title = tr("Continue Watching")
|
||||||
|
m.homeSectionIndexes.AddReplace("resume", m.homeSectionIndexes.count)
|
||||||
|
m.homeSectionIndexes.count++
|
||||||
|
sizeArray.push([464, 331])
|
||||||
|
|
||||||
' Load the Continue Watching Data
|
' Load the Continue Watching Data
|
||||||
m.LoadContinueTask.observeField("content", "updateContinueItems")
|
m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
|
||||||
m.LoadContinueTask.control = "RUN"
|
m.LoadContinueWatchingTask.control = "RUN"
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Create a row displaying next episodes up to watch
|
||||||
|
sub createNextUpRow(content as dynamic, sizeArray as dynamic)
|
||||||
|
nextUpRow = content.CreateChild("HomeRow")
|
||||||
|
nextUpRow.title = tr("Next Up >")
|
||||||
|
m.homeSectionIndexes.AddReplace("nextup", m.homeSectionIndexes.count)
|
||||||
|
m.homeSectionIndexes.count++
|
||||||
|
sizeArray.push([464, 331])
|
||||||
|
|
||||||
|
' Load the Next Up Data
|
||||||
|
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
||||||
|
m.LoadNextUpTask.control = "RUN"
|
||||||
|
end sub
|
||||||
|
|
||||||
|
' Create a row displaying items from the user's favorites list
|
||||||
|
sub createFavoritesRow(content as dynamic, sizeArray as dynamic)
|
||||||
|
favoritesRow = content.CreateChild("HomeRow")
|
||||||
|
favoritesRow.title = tr("Favorites")
|
||||||
|
sizeArray.Push([464, 331])
|
||||||
|
|
||||||
|
m.homeSectionIndexes.AddReplace("favorites", m.homeSectionIndexes.count)
|
||||||
|
m.homeSectionIndexes.count++
|
||||||
|
|
||||||
' Load the Favorites Data
|
' Load the Favorites Data
|
||||||
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
||||||
m.LoadFavoritesTask.control = "RUN"
|
m.LoadFavoritesTask.control = "RUN"
|
||||||
|
|
||||||
' If we have Live TV access, load "On Now" data
|
|
||||||
if haveLiveTV
|
|
||||||
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
|
||||||
m.LoadOnNowTask.control = "RUN"
|
|
||||||
end if
|
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
' Update home row data
|
||||||
sub updateHomeRows()
|
sub updateHomeRows()
|
||||||
if m.global.playstateTask.state = "run"
|
if m.global.playstateTask.state = "run"
|
||||||
m.global.playstateTask.observeField("state", "updateHomeRows")
|
m.global.playstateTask.observeField("state", "updateHomeRows")
|
||||||
|
@ -136,8 +266,43 @@ sub updateHomeRows()
|
||||||
|
|
||||||
m.global.playstateTask.unobserveField("state")
|
m.global.playstateTask.unobserveField("state")
|
||||||
|
|
||||||
m.LoadContinueTask.observeField("content", "updateContinueItems")
|
' If resume section exists, reload row's data
|
||||||
m.LoadContinueTask.control = "RUN"
|
if m.homeSectionIndexes.doesExist("resume")
|
||||||
|
m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
|
||||||
|
m.LoadContinueWatchingTask.control = "RUN"
|
||||||
|
end if
|
||||||
|
|
||||||
|
' If next up section exists, reload row's data
|
||||||
|
if m.homeSectionIndexes.doesExist("nextup")
|
||||||
|
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
||||||
|
m.LoadNextUpTask.control = "RUN"
|
||||||
|
end if
|
||||||
|
|
||||||
|
' If favorites section exists, reload row's data
|
||||||
|
if m.homeSectionIndexes.doesExist("favorites")
|
||||||
|
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
||||||
|
m.LoadFavoritesTask.control = "RUN"
|
||||||
|
end if
|
||||||
|
|
||||||
|
' If live tv's on now section exists, reload row's data
|
||||||
|
if m.homeSectionIndexes.doesExist("livetv")
|
||||||
|
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
||||||
|
m.LoadOnNowTask.control = "RUN"
|
||||||
|
end if
|
||||||
|
|
||||||
|
' If latest in library section exists, reload row's data
|
||||||
|
hasLatestHomeSection = false
|
||||||
|
|
||||||
|
for each section in m.homeSectionIndexes
|
||||||
|
if LCase(Left(section, 6)) = "latest"
|
||||||
|
hasLatestHomeSection = true
|
||||||
|
exit for
|
||||||
|
end if
|
||||||
|
end for
|
||||||
|
|
||||||
|
if hasLatestHomeSection
|
||||||
|
updateLatestInRows()
|
||||||
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub updateFavoritesItems()
|
sub updateFavoritesItems()
|
||||||
|
@ -147,20 +312,15 @@ sub updateFavoritesItems()
|
||||||
|
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
homeRows = m.top.content
|
rowIndex = m.homeSectionIndexes.favorites
|
||||||
rowIndex = getRowIndex("Favorites")
|
|
||||||
|
|
||||||
if itemData.count() < 1
|
if itemData.count() < 1
|
||||||
if isValid(rowIndex)
|
removeHomeSection("favorites")
|
||||||
' remove the row
|
return
|
||||||
deleteFromSizeArray(rowIndex)
|
|
||||||
homeRows.removeChildIndex(rowIndex)
|
|
||||||
end if
|
|
||||||
else
|
else
|
||||||
' remake row using the new data
|
' remake row using the new data
|
||||||
row = CreateObject("roSGNode", "HomeRow")
|
row = CreateObject("roSGNode", "HomeRow")
|
||||||
row.title = tr("Favorites")
|
row.title = tr("Favorites")
|
||||||
itemSize = [464, 331]
|
|
||||||
|
|
||||||
for each item in itemData
|
for each item in itemData
|
||||||
usePoster = true
|
usePoster = true
|
||||||
|
@ -174,60 +334,40 @@ sub updateFavoritesItems()
|
||||||
row.appendChild(item)
|
row.appendChild(item)
|
||||||
end for
|
end for
|
||||||
|
|
||||||
if rowIndex = invalid
|
' replace the old row
|
||||||
' insert new row under "My Media"
|
m.top.content.replaceChild(row, rowIndex)
|
||||||
updateSizeArray(itemSize, 1)
|
|
||||||
homeRows.insertChild(row, 1)
|
|
||||||
else
|
|
||||||
' replace the old row
|
|
||||||
homeRows.replaceChild(row, rowIndex)
|
|
||||||
end if
|
|
||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub updateContinueItems()
|
sub updateContinueWatchingItems()
|
||||||
itemData = m.LoadContinueTask.content
|
itemData = m.LoadContinueWatchingTask.content
|
||||||
m.LoadContinueTask.unobserveField("content")
|
m.LoadContinueWatchingTask.unobserveField("content")
|
||||||
m.LoadContinueTask.content = []
|
m.LoadContinueWatchingTask.content = []
|
||||||
|
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
homeRows = m.top.content
|
|
||||||
continueRowIndex = getRowIndex("Continue Watching")
|
|
||||||
|
|
||||||
if itemData.count() < 1
|
if itemData.count() < 1
|
||||||
if isValid(continueRowIndex)
|
removeHomeSection("resume")
|
||||||
' remove the row
|
return
|
||||||
deleteFromSizeArray(continueRowIndex)
|
|
||||||
homeRows.removeChildIndex(continueRowIndex)
|
|
||||||
end if
|
|
||||||
else
|
|
||||||
' remake row using the new data
|
|
||||||
row = CreateObject("roSGNode", "HomeRow")
|
|
||||||
row.title = tr("Continue Watching")
|
|
||||||
itemSize = [464, 331]
|
|
||||||
for each item in itemData
|
|
||||||
if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
|
|
||||||
item.PlayedPercentage = item.json.UserData.PlayedPercentage
|
|
||||||
end if
|
|
||||||
|
|
||||||
item.usePoster = row.usePoster
|
|
||||||
item.imageWidth = row.imageWidth
|
|
||||||
row.appendChild(item)
|
|
||||||
end for
|
|
||||||
|
|
||||||
if continueRowIndex = invalid
|
|
||||||
' insert new row under "My Media"
|
|
||||||
updateSizeArray(itemSize, 1)
|
|
||||||
homeRows.insertChild(row, 1)
|
|
||||||
else
|
|
||||||
' replace the old row
|
|
||||||
homeRows.replaceChild(row, continueRowIndex)
|
|
||||||
end if
|
|
||||||
end if
|
end if
|
||||||
|
|
||||||
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
' remake row using the new data
|
||||||
m.LoadNextUpTask.control = "RUN"
|
row = CreateObject("roSGNode", "HomeRow")
|
||||||
|
row.title = tr("Continue Watching")
|
||||||
|
|
||||||
|
for each item in itemData
|
||||||
|
if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
|
||||||
|
item.PlayedPercentage = item.json.UserData.PlayedPercentage
|
||||||
|
end if
|
||||||
|
|
||||||
|
item.usePoster = row.usePoster
|
||||||
|
item.imageWidth = row.imageWidth
|
||||||
|
row.appendChild(item)
|
||||||
|
end for
|
||||||
|
|
||||||
|
' replace the old row
|
||||||
|
m.top.content.replaceChild(row, m.homeSectionIndexes.resume)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub updateNextUpItems()
|
sub updateNextUpItems()
|
||||||
|
@ -237,53 +377,33 @@ sub updateNextUpItems()
|
||||||
|
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
homeRows = m.top.content
|
|
||||||
nextUpRowIndex = getRowIndex("Next Up >")
|
|
||||||
|
|
||||||
if itemData.count() < 1
|
if itemData.count() < 1
|
||||||
if isValid(nextUpRowIndex)
|
removeHomeSection("nextup")
|
||||||
' remove the row
|
return
|
||||||
deleteFromSizeArray(nextUpRowIndex)
|
|
||||||
homeRows.removeChildIndex(nextUpRowIndex)
|
|
||||||
end if
|
|
||||||
else
|
else
|
||||||
' remake row using the new data
|
' remake row using the new data
|
||||||
row = CreateObject("roSGNode", "HomeRow")
|
row = CreateObject("roSGNode", "HomeRow")
|
||||||
row.title = tr("Next Up") + " >"
|
row.title = tr("Next Up") + " >"
|
||||||
itemSize = [464, 331]
|
|
||||||
for each item in itemData
|
for each item in itemData
|
||||||
item.usePoster = row.usePoster
|
item.usePoster = row.usePoster
|
||||||
item.imageWidth = row.imageWidth
|
item.imageWidth = row.imageWidth
|
||||||
row.appendChild(item)
|
row.appendChild(item)
|
||||||
end for
|
end for
|
||||||
|
|
||||||
if nextUpRowIndex = invalid
|
' replace the old row
|
||||||
' insert new row under "Continue Watching"
|
m.top.content.replaceChild(row, m.homeSectionIndexes.nextup)
|
||||||
continueRowIndex = getRowIndex("Continue Watching")
|
|
||||||
if isValid(continueRowIndex)
|
|
||||||
updateSizeArray(itemSize, continueRowIndex + 1)
|
|
||||||
homeRows.insertChild(row, continueRowIndex + 1)
|
|
||||||
else
|
|
||||||
' insert it under My Media
|
|
||||||
updateSizeArray(itemSize, 1)
|
|
||||||
homeRows.insertChild(row, 1)
|
|
||||||
end if
|
|
||||||
else
|
|
||||||
' replace the old row
|
|
||||||
homeRows.replaceChild(row, nextUpRowIndex)
|
|
||||||
end if
|
|
||||||
end if
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
' consider home screen loaded when above rows are loaded
|
' Iterate over user's libraries and update data for each Latest In section
|
||||||
if m.global.app_loaded = false
|
sub updateLatestInRows()
|
||||||
m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
|
' Ensure we have data
|
||||||
m.global.app_loaded = true
|
if not isValidAndNotEmpty(m.libraryData) then return
|
||||||
end if
|
|
||||||
|
|
||||||
' create task nodes for "Latest In" rows
|
' Load new data for each library
|
||||||
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
|
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
|
||||||
for each lib in filteredLatest
|
for each lib in filteredLatest
|
||||||
if lib.collectionType <> "livetv" and lib.collectionType <> "boxsets" and lib.json.CollectionType <> "Program"
|
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
|
||||||
loadLatest = createObject("roSGNode", "LoadItemsTask")
|
loadLatest = createObject("roSGNode", "LoadItemsTask")
|
||||||
loadLatest.itemsToLoad = "latest"
|
loadLatest.itemsToLoad = "latest"
|
||||||
loadLatest.itemId = lib.id
|
loadLatest.itemId = lib.id
|
||||||
|
@ -296,7 +416,6 @@ sub updateNextUpItems()
|
||||||
loadLatest.control = "RUN"
|
loadLatest.control = "RUN"
|
||||||
end if
|
end if
|
||||||
end for
|
end for
|
||||||
|
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub updateLatestItems(msg)
|
sub updateLatestItems(msg)
|
||||||
|
@ -308,15 +427,13 @@ sub updateLatestItems(msg)
|
||||||
|
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
homeRows = m.top.content
|
sectionName = "latestin" + LCase(node.metadata.title).Replace(" ", "")
|
||||||
rowIndex = getRowIndex(tr("Latest in") + " " + node.metadata.title + " >")
|
|
||||||
|
rowIndex = m.homeSectionIndexes[sectionName]
|
||||||
|
|
||||||
if itemData.count() < 1
|
if itemData.count() < 1
|
||||||
' remove row
|
removeHomeSection(sectionName)
|
||||||
if isValid(rowIndex)
|
return
|
||||||
deleteFromSizeArray(rowIndex)
|
|
||||||
homeRows.removeChildIndex(rowIndex)
|
|
||||||
end if
|
|
||||||
else
|
else
|
||||||
' remake row using new data
|
' remake row using new data
|
||||||
row = CreateObject("roSGNode", "HomeRow")
|
row = CreateObject("roSGNode", "HomeRow")
|
||||||
|
@ -340,15 +457,9 @@ sub updateLatestItems(msg)
|
||||||
row.appendChild(item)
|
row.appendChild(item)
|
||||||
end for
|
end for
|
||||||
|
|
||||||
if rowIndex = invalid
|
' replace the old row
|
||||||
' append new row
|
updateSizeArray(itemSize, rowIndex, "replace")
|
||||||
updateSizeArray(itemSize)
|
m.top.content.replaceChild(row, rowIndex)
|
||||||
homeRows.appendChild(row)
|
|
||||||
else
|
|
||||||
' replace the old row
|
|
||||||
updateSizeArray(itemSize, rowIndex, "replace")
|
|
||||||
homeRows.replaceChild(row, rowIndex)
|
|
||||||
end if
|
|
||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
@ -359,50 +470,25 @@ sub updateOnNowItems()
|
||||||
|
|
||||||
if itemData = invalid then return
|
if itemData = invalid then return
|
||||||
|
|
||||||
homeRows = m.top.content
|
|
||||||
onNowRowIndex = getRowIndex("On Now")
|
|
||||||
|
|
||||||
if itemData.count() < 1
|
if itemData.count() < 1
|
||||||
if isValid(onNowRowIndex)
|
removeHomeSection("livetv")
|
||||||
' remove the row
|
return
|
||||||
deleteFromSizeArray(onNowRowIndex)
|
|
||||||
homeRows.removeChildIndex(onNowRowIndex)
|
|
||||||
end if
|
|
||||||
else
|
else
|
||||||
' remake row using the new data
|
' remake row using the new data
|
||||||
row = CreateObject("roSGNode", "HomeRow")
|
row = CreateObject("roSGNode", "HomeRow")
|
||||||
row.title = tr("On Now")
|
row.title = tr("On Now")
|
||||||
itemSize = [464, 331]
|
|
||||||
for each item in itemData
|
for each item in itemData
|
||||||
item.usePoster = row.usePoster
|
item.usePoster = row.usePoster
|
||||||
item.imageWidth = row.imageWidth
|
item.imageWidth = row.imageWidth
|
||||||
row.appendChild(item)
|
row.appendChild(item)
|
||||||
end for
|
end for
|
||||||
|
|
||||||
if onNowRowIndex = invalid
|
' replace the old row
|
||||||
' insert new row under "My Media"
|
m.top.content.replaceChild(row, m.homeSectionIndexes.livetv)
|
||||||
updateSizeArray(itemSize, 1)
|
|
||||||
homeRows.insertChild(row, 1)
|
|
||||||
else
|
|
||||||
' replace the old row
|
|
||||||
homeRows.replaceChild(row, onNowRowIndex)
|
|
||||||
end if
|
|
||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function getRowIndex(rowTitle as string)
|
|
||||||
rowIndex = invalid
|
|
||||||
for i = 1 to m.top.content.getChildCount() - 1
|
|
||||||
' skip row 0 since it's always "My Media"
|
|
||||||
tmpRow = m.top.content.getChild(i)
|
|
||||||
if tmpRow.title = rowTitle
|
|
||||||
rowIndex = i
|
|
||||||
exit for
|
|
||||||
end if
|
|
||||||
end for
|
|
||||||
return rowIndex
|
|
||||||
end function
|
|
||||||
|
|
||||||
sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
|
sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
|
||||||
sizeArray = m.top.rowItemSize
|
sizeArray = m.top.rowItemSize
|
||||||
' append by default
|
' append by default
|
||||||
|
@ -428,10 +514,6 @@ sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
|
||||||
m.top.rowItemSize = newSizeArray
|
m.top.rowItemSize = newSizeArray
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub deleteFromSizeArray(rowIndex)
|
|
||||||
updateSizeArray([0, 0], rowIndex, "delete")
|
|
||||||
end sub
|
|
||||||
|
|
||||||
sub itemSelected()
|
sub itemSelected()
|
||||||
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
|
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
|
||||||
|
|
||||||
|
@ -440,21 +522,20 @@ sub itemSelected()
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
handled = false
|
|
||||||
if press
|
if press
|
||||||
if key = "play"
|
if key = "play"
|
||||||
|
print "play was pressed from homerow"
|
||||||
itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
|
itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
|
||||||
if isValid(itemToPlay) and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
if isValid(itemToPlay)
|
||||||
m.top.quickPlayNode = itemToPlay
|
m.top.quickPlayNode = itemToPlay
|
||||||
end if
|
end if
|
||||||
handled = true
|
return true
|
||||||
end if
|
else if key = "replay"
|
||||||
|
|
||||||
if key = "replay"
|
|
||||||
m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0]
|
m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0]
|
||||||
|
return true
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
return handled
|
return false
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function filterNodeArray(nodeArray as object, nodeKey as string, excludeArray as object) as object
|
function filterNodeArray(nodeArray as object, nodeKey as string, excludeArray as object) as object
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<component name="HomeRows" extends="RowList">
|
<component name="HomeRows" extends="RowList">
|
||||||
<interface>
|
<interface>
|
||||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||||
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
<field id="quickPlayNode" type="node" />
|
||||||
<function name="updateHomeRows" />
|
<function name="updateHomeRows" />
|
||||||
<function name="loadLibraries" />
|
<function name="loadLibraries" />
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
@ -137,7 +137,8 @@ sub loadItems()
|
||||||
if isValid(data) and isValid(data.Items)
|
if isValid(data) and isValid(data.Items)
|
||||||
for each item in data.Items
|
for each item in data.Items
|
||||||
' Skip Books for now as we don't support it (issue #558)
|
' Skip Books for now as we don't support it (issue #558)
|
||||||
if item.Type <> "Book"
|
' also skip songs since there is limited space
|
||||||
|
if not (item.Type = "Book" or item.Type = "Audio")
|
||||||
tmp = CreateObject("roSGNode", "HomeData")
|
tmp = CreateObject("roSGNode", "HomeData")
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
|
|
81
components/keyboards/IntegerKeyboard.bs
Normal file
81
components/keyboards/IntegerKeyboard.bs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
sub init()
|
||||||
|
m.top.keyGrid.keyDefinitionUri = "pkg:/components/keyboards/IntegerKeyboardKDF.json"
|
||||||
|
end sub
|
||||||
|
|
||||||
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
|
if key = "back"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
if not press then return false
|
||||||
|
|
||||||
|
if key = "left"
|
||||||
|
if m.top.textEditBox.hasFocus()
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "1"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "4"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "7"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "backspace"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
|
if key = "right"
|
||||||
|
if m.top.textEditBox.hasFocus()
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "3"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "6"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "9"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "submit"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
|
if key = "up"
|
||||||
|
if m.top.textEditBox.hasFocus()
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
|
if key = "down"
|
||||||
|
if m.top.focusedChild.keyFocused = "0"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "backspace"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
else if m.top.focusedChild.keyFocused = "submit"
|
||||||
|
m.top.escape = key
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
|
return false
|
||||||
|
end function
|
||||||
|
|
||||||
|
function keySelected(key as string) as boolean
|
||||||
|
if key = "submit"
|
||||||
|
m.top.submit = true
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
return false
|
||||||
|
end function
|
9
components/keyboards/IntegerKeyboard.xml
Normal file
9
components/keyboards/IntegerKeyboard.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
|
||||||
|
<component name="IntegerKeyboard" extends="DynamicCustomKeyboard">
|
||||||
|
<interface>
|
||||||
|
<function name="keySelected" />
|
||||||
|
<field id="submit" type="bool" alwaysNotify="true" />
|
||||||
|
<field id="escape" type="string" alwaysNotify="true" />
|
||||||
|
</interface>
|
||||||
|
</component>
|
73
components/keyboards/IntegerKeyboardKDF.json
Normal file
73
components/keyboards/IntegerKeyboardKDF.json
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"keyboardWidthFHD": 495,
|
||||||
|
"keyboardHeightFHD": 300,
|
||||||
|
"keyboardWidthHD": 324,
|
||||||
|
"keyboardHeightHD": 200,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"grids": [
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"label": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"label": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "6"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"label": "7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"icon": "theme:DKB_DeleteKeyBitmap",
|
||||||
|
"focusIcon": "theme:DKB_DeleteKeyFocusBitmap",
|
||||||
|
"autoRepeat": 1,
|
||||||
|
"strOut": "backspace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "pkg:/images/icons/check_white.png",
|
||||||
|
"focusIcon": "pkg:/images/icons/check_black.png",
|
||||||
|
"strOut": "submit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ sub init()
|
||||||
m.queue = []
|
m.queue = []
|
||||||
m.originalQueue = []
|
m.originalQueue = []
|
||||||
m.queueTypes = []
|
m.queueTypes = []
|
||||||
|
m.isPlaying = false
|
||||||
' Preroll videos only play if user has cinema mode setting enabled
|
' Preroll videos only play if user has cinema mode setting enabled
|
||||||
m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
|
m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
|
||||||
m.position = 0
|
m.position = 0
|
||||||
|
@ -19,6 +20,7 @@ end sub
|
||||||
|
|
||||||
' Clear all content from play queue
|
' Clear all content from play queue
|
||||||
sub clear()
|
sub clear()
|
||||||
|
m.isPlaying = false
|
||||||
m.queue = []
|
m.queue = []
|
||||||
m.queueTypes = []
|
m.queueTypes = []
|
||||||
m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
|
m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"]
|
||||||
|
@ -111,6 +113,7 @@ end function
|
||||||
|
|
||||||
' Play items in queue
|
' Play items in queue
|
||||||
sub playQueue()
|
sub playQueue()
|
||||||
|
m.isPlaying = true
|
||||||
nextItem = getCurrentItem()
|
nextItem = getCurrentItem()
|
||||||
if not isValid(nextItem) then return
|
if not isValid(nextItem) then return
|
||||||
|
|
||||||
|
@ -122,11 +125,21 @@ sub playQueue()
|
||||||
return
|
return
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
if nextItemMediaType = "musicvideo"
|
||||||
|
CreateVideoPlayerView()
|
||||||
|
return
|
||||||
|
end if
|
||||||
|
|
||||||
if nextItemMediaType = "video"
|
if nextItemMediaType = "video"
|
||||||
CreateVideoPlayerView()
|
CreateVideoPlayerView()
|
||||||
return
|
return
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
if nextItemMediaType = "movie"
|
||||||
|
CreateVideoPlayerView()
|
||||||
|
return
|
||||||
|
end if
|
||||||
|
|
||||||
if nextItemMediaType = "episode"
|
if nextItemMediaType = "episode"
|
||||||
CreateVideoPlayerView()
|
CreateVideoPlayerView()
|
||||||
return
|
return
|
||||||
|
@ -196,21 +209,25 @@ end function
|
||||||
sub shuffleQueueItems()
|
sub shuffleQueueItems()
|
||||||
' By calling getQueue 2 different ways, Roku avoids needing to do a deep copy
|
' By calling getQueue 2 different ways, Roku avoids needing to do a deep copy
|
||||||
m.originalQueue = m.global.queueManager.callFunc("getQueue")
|
m.originalQueue = m.global.queueManager.callFunc("getQueue")
|
||||||
songIDArray = getQueue()
|
itemIDArray = getQueue()
|
||||||
|
temp = invalid
|
||||||
|
|
||||||
' Move the currently playing song to the front of the queue
|
if m.isPlaying
|
||||||
temp = top()
|
' Save the currently playing item
|
||||||
songIDArray[0] = getCurrentItem()
|
temp = getCurrentItem()
|
||||||
songIDArray[getPosition()] = temp
|
' remove currently playing item from itemIDArray
|
||||||
|
itemIDArray.Delete(m.position)
|
||||||
|
end if
|
||||||
|
|
||||||
for i = 1 to songIDArray.count() - 1
|
' shuffle all items
|
||||||
j = Rnd(songIDArray.count() - 1)
|
itemIDArray = shuffleArray(itemIDArray)
|
||||||
temp = songIDArray[i]
|
|
||||||
songIDArray[i] = songIDArray[j]
|
|
||||||
songIDArray[j] = temp
|
|
||||||
end for
|
|
||||||
|
|
||||||
set(songIDArray)
|
if m.isPlaying
|
||||||
|
' Put currently playing item in front of itemIDArray
|
||||||
|
itemIDArray.Unshift(temp)
|
||||||
|
end if
|
||||||
|
|
||||||
|
set(itemIDArray)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
' Return the fitst item in the play queue
|
' Return the fitst item in the play queue
|
||||||
|
|
|
@ -385,6 +385,12 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
audioOptionsClosed()
|
audioOptionsClosed()
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
else if key = "play" and m.extrasGrid.hasFocus()
|
||||||
|
print "Play was pressed from the movie details extras slider"
|
||||||
|
if m.extrasGrid.focusedItem <> invalid
|
||||||
|
m.top.quickPlayNode = m.extrasGrid.focusedItem
|
||||||
|
return true
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
return false
|
return false
|
||||||
end function
|
end function
|
||||||
|
|
|
@ -50,5 +50,6 @@
|
||||||
<field id="trailerAvailable" type="bool" onChange="trailerAvailableChanged" value="false" />
|
<field id="trailerAvailable" type="bool" onChange="trailerAvailableChanged" value="false" />
|
||||||
<field id="selectedAudioStreamIndex" type="integer" />
|
<field id="selectedAudioStreamIndex" type="integer" />
|
||||||
<field id="selectedVideoStreamId" type="string" />
|
<field id="selectedVideoStreamId" type="string" />
|
||||||
|
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||||
</interface>
|
</interface>
|
||||||
</component>
|
</component>
|
|
@ -313,5 +313,21 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
if key = "play"
|
||||||
|
print "play button pressed from ArtistView"
|
||||||
|
itemToPlay = invalid
|
||||||
|
|
||||||
|
if isValid(m.albums) and m.albums.isInFocusChain()
|
||||||
|
itemToPlay = m.albums.MusicArtistAlbumData.items[m.albums.itemFocused]
|
||||||
|
else if isValid(m.appearsOn) and m.appearsOn.isInFocusChain()
|
||||||
|
itemToPlay = m.appearsOn.MusicArtistAlbumData.items[m.appearsOn.itemFocused]
|
||||||
|
end if
|
||||||
|
|
||||||
|
if isValid(itemToPlay)
|
||||||
|
m.top.quickPlayNode = itemToPlay
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
|
||||||
return false
|
return false
|
||||||
end function
|
end function
|
||||||
|
|
|
@ -33,11 +33,11 @@
|
||||||
|
|
||||||
</SectionScroller>
|
</SectionScroller>
|
||||||
|
|
||||||
<bgv_ButtonGroupVert id="sectionNavigation" translation="[-100, 175]" itemSpacings="[10]">
|
<ButtonGroupVert id="sectionNavigation" translation="[-100, 175]" itemSpacings="[10]">
|
||||||
<sob_SlideOutButton background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/details.png" text="Details" height="50" width="60" />
|
<SlideOutButton background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/details.png" text="Details" height="50" width="60" />
|
||||||
<sob_SlideOutButton id="albumsLink" background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/cd.png" text="Albums" height="50" width="60" />
|
<SlideOutButton id="albumsLink" background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/cd.png" text="Albums" height="50" width="60" />
|
||||||
<sob_SlideOutButton id="appearsOnLink" background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/cassette.png" text="Appears On" height="50" width="60" />
|
<SlideOutButton id="appearsOnLink" background="#070707" focusBackground="#00a4dc" highlightBackground="#555555" padding="20" icon="pkg:/images/icons/cassette.png" text="Appears On" height="50" width="60" />
|
||||||
</bgv_ButtonGroupVert>
|
</ButtonGroupVert>
|
||||||
|
|
||||||
<Animation id="pageLoad" duration="1" repeat="false">
|
<Animation id="pageLoad" duration="1" repeat="false">
|
||||||
<Vector2DFieldInterpolator key="[0.5, 1.0]" keyValue="[[-100, 175], [40, 175]]" fieldToInterp="sectionNavigation.translation" />
|
<Vector2DFieldInterpolator key="[0.5, 1.0]" keyValue="[[-100, 175], [40, 175]]" fieldToInterp="sectionNavigation.translation" />
|
||||||
|
@ -54,5 +54,6 @@
|
||||||
<field id="playArtistSelected" alias="play.selected" />
|
<field id="playArtistSelected" alias="play.selected" />
|
||||||
<field id="instantMixSelected" alias="instantMix.selected" />
|
<field id="instantMixSelected" alias="instantMix.selected" />
|
||||||
<field id="selectedButtonIndex" type="integer" value="-1" />
|
<field id="selectedButtonIndex" type="integer" value="-1" />
|
||||||
|
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||||
</interface>
|
</interface>
|
||||||
</component>
|
</component>
|
|
@ -34,7 +34,7 @@ sub OnAuthenticated()
|
||||||
if authenticated <> invalid and authenticated = true
|
if authenticated <> invalid and authenticated = true
|
||||||
currentUser = AboutMe()
|
currentUser = AboutMe()
|
||||||
session.user.Login(currentUser)
|
session.user.Login(currentUser)
|
||||||
LoadUserPreferences()
|
session.user.LoadUserPreferences()
|
||||||
LoadUserAbilities()
|
LoadUserAbilities()
|
||||||
m.top.close = true
|
m.top.close = true
|
||||||
m.top.authenticated = true
|
m.top.authenticated = true
|
||||||
|
|
|
@ -41,6 +41,13 @@ sub loadResults()
|
||||||
m.searchSelect.itemdata = m.searchTask.results
|
m.searchSelect.itemdata = m.searchTask.results
|
||||||
m.searchSelect.query = m.top.SearchAlpha
|
m.searchSelect.query = m.top.SearchAlpha
|
||||||
m.searchHelpText.visible = false
|
m.searchHelpText.visible = false
|
||||||
|
if m.searchTask.results.TotalRecordCount = 0
|
||||||
|
' make sure focus is on the keyboard
|
||||||
|
if m.searchSelect.isinFocusChain()
|
||||||
|
m.searchAlphabox.setFocus(true)
|
||||||
|
end if
|
||||||
|
return
|
||||||
|
end if
|
||||||
m.searchAlphabox = m.top.findnode("searchResults")
|
m.searchAlphabox = m.top.findnode("searchResults")
|
||||||
m.searchAlphabox.translation = "[470, 85]"
|
m.searchAlphabox.translation = "[470, 85]"
|
||||||
end sub
|
end sub
|
||||||
|
@ -57,9 +64,21 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
if key = "left" and m.searchSelect.isinFocusChain()
|
if key = "left" and m.searchSelect.isinFocusChain()
|
||||||
m.searchAlphabox.setFocus(true)
|
m.searchAlphabox.setFocus(true)
|
||||||
return true
|
return true
|
||||||
else if key = "right"
|
else if key = "right" and m.searchSelect.content <> invalid and m.searchSelect.content.getChildCount() > 0
|
||||||
m.searchSelect.setFocus(true)
|
m.searchSelect.setFocus(true)
|
||||||
return true
|
return true
|
||||||
|
else if key = "play" and m.searchSelect.isinFocusChain() and m.searchSelect.rowItemFocused.count() > 0
|
||||||
|
print "play was pressed from search results"
|
||||||
|
if m.searchSelect.rowItemFocused <> invalid
|
||||||
|
selectedContent = m.searchSelect.content.getChild(m.searchSelect.rowItemFocused[0])
|
||||||
|
if selectedContent <> invalid
|
||||||
|
selectedItem = selectedContent.getChild(m.searchSelect.rowItemFocused[1])
|
||||||
|
if selectedItem <> invalid
|
||||||
|
m.top.quickPlayNode = selectedItem
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ function getData()
|
||||||
"PlaylistsFolder": { "label": "Playlist", "count": 0 }
|
"PlaylistsFolder": { "label": "Playlist", "count": 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
for each item in itemData.searchHints
|
for each item in itemData.Items
|
||||||
if content_types[item.type] <> invalid
|
if content_types[item.type] <> invalid
|
||||||
content_types[item.type].count += 1
|
content_types[item.type].count += 1
|
||||||
end if
|
end if
|
||||||
|
@ -86,10 +86,9 @@ sub addRow(data, title, type_filter)
|
||||||
itemData = m.top.itemData
|
itemData = m.top.itemData
|
||||||
row = data.CreateChild("ContentNode")
|
row = data.CreateChild("ContentNode")
|
||||||
row.title = title
|
row.title = title
|
||||||
for each item in itemData.SearchHints
|
for each item in itemData.Items
|
||||||
if item.type = type_filter
|
if item.type = type_filter
|
||||||
row.appendChild(item)
|
row.appendChild(item)
|
||||||
end if
|
end if
|
||||||
end for
|
end for
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import "pkg:/source/utils/config.bs"
|
import "pkg:/source/utils/config.bs"
|
||||||
import "pkg:/source/utils/misc.bs"
|
import "pkg:/source/utils/misc.bs"
|
||||||
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
||||||
import "pkg:/source/api/sdk.bs"
|
|
||||||
|
|
||||||
sub init()
|
sub init()
|
||||||
m.log = log.Logger("Settings")
|
m.log = log.Logger("Settings")
|
||||||
|
@ -202,16 +201,21 @@ sub radioSettingChanged()
|
||||||
set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id)
|
set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id)
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
' Returns true if any of the data entry forms are in focus
|
||||||
|
function isFormInFocus() as boolean
|
||||||
|
if isValid(m.settingDetail.focusedChild) or m.radioSetting.hasFocus() or m.boolSetting.hasFocus() or m.integerSetting.hasFocus()
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
return false
|
||||||
|
end function
|
||||||
|
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
if not press then return false
|
if not press then return false
|
||||||
|
|
||||||
if (key = "back" or key = "left") and m.settingsMenu.focusedChild <> invalid and m.userLocation.Count() > 1
|
if (key = "back" or key = "left") and m.settingsMenu.focusedChild <> invalid and m.userLocation.Count() > 1
|
||||||
LoadMenu({})
|
LoadMenu({})
|
||||||
return true
|
return true
|
||||||
else if (key = "back" or key = "left") and m.settingDetail.focusedChild <> invalid
|
else if (key = "back" or key = "left") and isFormInFocus()
|
||||||
m.settingsMenu.setFocus(true)
|
|
||||||
return true
|
|
||||||
else if (key = "back" or key = "left") and m.radioSetting.hasFocus()
|
|
||||||
m.settingsMenu.setFocus(true)
|
m.settingsMenu.setFocus(true)
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</ContentNode>
|
</ContentNode>
|
||||||
</RadioButtonList>
|
</RadioButtonList>
|
||||||
|
|
||||||
<intkeyboard_integerKeyboard translation="[1034, 510]" id="integerSetting" maxLength="3" domain="numeric" visible="false" />
|
<IntegerKeyboard translation="[1034, 510]" id="integerSetting" maxLength="3" domain="numeric" visible="false" />
|
||||||
|
|
||||||
</children>
|
</children>
|
||||||
</component>
|
</component>
|
|
@ -2,13 +2,15 @@ import "pkg:/source/api/Image.bs"
|
||||||
import "pkg:/source/api/baserequest.bs"
|
import "pkg:/source/api/baserequest.bs"
|
||||||
import "pkg:/source/utils/config.bs"
|
import "pkg:/source/utils/config.bs"
|
||||||
import "pkg:/source/utils/misc.bs"
|
import "pkg:/source/utils/misc.bs"
|
||||||
|
import "pkg:/source/api/sdk.bs"
|
||||||
|
|
||||||
sub init()
|
sub init()
|
||||||
m.top.optionsAvailable = false
|
m.top.optionsAvailable = false
|
||||||
|
|
||||||
m.rows = m.top.findNode("picker")
|
m.rows = m.top.findNode("picker")
|
||||||
m.poster = m.top.findNode("seasonPoster")
|
m.poster = m.top.findNode("seasonPoster")
|
||||||
m.Shuffle = m.top.findNode("Shuffle")
|
m.shuffle = m.top.findNode("shuffle")
|
||||||
|
m.extras = m.top.findNode("extras")
|
||||||
m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
|
m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
|
||||||
|
|
||||||
m.unplayedCount = m.top.findNode("unplayedCount")
|
m.unplayedCount = m.top.findNode("unplayedCount")
|
||||||
|
@ -21,6 +23,13 @@ sub setSeasonLoading()
|
||||||
m.top.overhangTitle = tr("Loading...")
|
m.top.overhangTitle = tr("Loading...")
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
' Updates the visibility of the Extras button based on if this season has any extra features
|
||||||
|
sub setExtraButtonVisibility()
|
||||||
|
if isValid(m.top.extrasObjects) and isValidAndNotEmpty(m.top.extrasObjects.items)
|
||||||
|
m.extras.visible = true
|
||||||
|
end if
|
||||||
|
end sub
|
||||||
|
|
||||||
sub updateSeason()
|
sub updateSeason()
|
||||||
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
if m.global.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"] = false
|
||||||
if isValid(m.top.seasonData) and isValid(m.top.seasonData.UserData) and isValid(m.top.seasonData.UserData.UnplayedItemCount)
|
if isValid(m.top.seasonData) and isValid(m.top.seasonData.UserData) and isValid(m.top.seasonData.UserData.UnplayedItemCount)
|
||||||
|
@ -33,26 +42,36 @@ sub updateSeason()
|
||||||
|
|
||||||
imgParams = { "maxHeight": 450, "maxWidth": 300 }
|
imgParams = { "maxHeight": 450, "maxWidth": 300 }
|
||||||
m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
|
m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
|
||||||
m.Shuffle.visible = true
|
m.shuffle.visible = true
|
||||||
m.top.overhangTitle = m.top.seasonData.SeriesName + " - " + m.top.seasonData.name
|
m.top.overhangTitle = m.top.seasonData.SeriesName + " - " + m.top.seasonData.name
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
|
' Handle navigation input from the remote and act on it
|
||||||
function onKeyEvent(key as string, press as boolean) as boolean
|
function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
handled = false
|
handled = false
|
||||||
|
|
||||||
if key = "left" and m.tvEpisodeRow.hasFocus()
|
if key = "left" and m.tvEpisodeRow.hasFocus()
|
||||||
m.Shuffle.setFocus(true)
|
m.shuffle.setFocus(true)
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if key = "right" and (m.Shuffle.hasFocus())
|
if key = "right" and (m.shuffle.hasFocus() or m.extras.hasFocus())
|
||||||
m.tvEpisodeRow.setFocus(true)
|
m.tvEpisodeRow.setFocus(true)
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if key = "OK" or key = "play"
|
if m.extras.visible and key = "up" and m.extras.hasFocus()
|
||||||
|
m.shuffle.setFocus(true)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
if m.Shuffle.hasFocus()
|
if m.extras.visible and key = "down" and m.shuffle.hasFocus()
|
||||||
|
m.extras.setFocus(true)
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
|
||||||
|
if key = "OK" or key = "play"
|
||||||
|
if m.shuffle.hasFocus()
|
||||||
episodeList = m.rows.getChild(0).objects.items
|
episodeList = m.rows.getChild(0).objects.items
|
||||||
|
|
||||||
for i = 0 to episodeList.count() - 1
|
for i = 0 to episodeList.count() - 1
|
||||||
|
@ -66,6 +85,16 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.global.queueManager.callFunc("playQueue")
|
m.global.queueManager.callFunc("playQueue")
|
||||||
return true
|
return true
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
if m.extras.visible and m.extras.hasFocus()
|
||||||
|
if LCase(m.extras.text.trim()) = LCase(tr("Extras"))
|
||||||
|
m.extras.text = tr("Episodes")
|
||||||
|
m.top.objects = m.top.extrasObjects
|
||||||
|
else
|
||||||
|
m.extras.text = tr("Extras")
|
||||||
|
m.top.objects = m.top.episodeObjects
|
||||||
|
end if
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
focusedChild = m.top.focusedChild.focusedChild
|
focusedChild = m.top.focusedChild.focusedChild
|
||||||
|
@ -81,7 +110,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
m.top.lastFocus = focusedChild
|
m.top.lastFocus = focusedChild
|
||||||
itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
|
itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
|
||||||
if isValid(itemToPlay) and isValid(itemToPlay.id) and itemToPlay.id <> ""
|
if isValid(itemToPlay) and isValid(itemToPlay.id) and itemToPlay.id <> ""
|
||||||
itemToPlay.type = "Episode"
|
|
||||||
m.top.quickPlayNode = itemToPlay
|
m.top.quickPlayNode = itemToPlay
|
||||||
end if
|
end if
|
||||||
handled = true
|
handled = true
|
||||||
|
|
|
@ -6,13 +6,16 @@
|
||||||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
|
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||||
</Rectangle>
|
</Rectangle>
|
||||||
</Poster>
|
</Poster>
|
||||||
<JFButton id="Shuffle" minChars="10" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
|
<JFButton id="shuffle" minChars="10" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
|
||||||
|
<JFButton id="extras" minChars="10" text="Extras" translation="[90, 740]" visible="false"></JFButton>
|
||||||
<TVEpisodeRowWithOptions id="picker" visible="true" />
|
<TVEpisodeRowWithOptions id="picker" visible="true" />
|
||||||
</children>
|
</children>
|
||||||
<interface>
|
<interface>
|
||||||
<field id="episodeSelected" alias="picker.itemSelected" />
|
<field id="episodeSelected" alias="picker.itemSelected" />
|
||||||
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
<field id="quickPlayNode" type="node" />
|
||||||
<field id="seasonData" type="assocarray" onChange="setSeasonLoading" />
|
<field id="seasonData" type="assocarray" onChange="setSeasonLoading" />
|
||||||
<field id="objects" alias="picker.objects" />
|
<field id="objects" alias="picker.objects" />
|
||||||
|
<field id="episodeObjects" type="assocarray" />
|
||||||
|
<field id="extrasObjects" type="assocarray" onChange="setExtraButtonVisibility" />
|
||||||
</interface>
|
</interface>
|
||||||
</component>
|
</component>
|
|
@ -11,6 +11,7 @@ sub init()
|
||||||
m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask")
|
m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask")
|
||||||
m.Shuffle = m.top.findNode("Shuffle")
|
m.Shuffle = m.top.findNode("Shuffle")
|
||||||
m.extrasSlider.visible = true
|
m.extrasSlider.visible = true
|
||||||
|
m.seasons = m.top.findNode("seasons")
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub itemContentChanged()
|
sub itemContentChanged()
|
||||||
|
@ -223,6 +224,20 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
else if key = "up" and m.Shuffle.hasFocus()
|
else if key = "up" and m.Shuffle.hasFocus()
|
||||||
overview.setFocus(true)
|
overview.setFocus(true)
|
||||||
return true
|
return true
|
||||||
|
else if key = "play" and m.seasons.hasFocus()
|
||||||
|
print "play was pressed from the seasons row"
|
||||||
|
if isValid(m.seasons.TVSeasonData) and isValid(m.seasons.TVSeasonData.Items)
|
||||||
|
itemFocused = m.seasons.rowItemFocused
|
||||||
|
m.top.quickPlayNode = m.seasons.TVSeasonData.Items[itemFocused[1]]
|
||||||
|
return true
|
||||||
|
end if
|
||||||
|
else if key = "play" and m.extrasSlider.isInFocusChain()
|
||||||
|
print "play was pressed from the extras grid"
|
||||||
|
extrasGrid = m.top.findNode("extrasGrid")
|
||||||
|
if extrasGrid.focusedItem <> invalid
|
||||||
|
m.top.quickPlayNode = extrasGrid.focusedItem
|
||||||
|
return true
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
||||||
<field id="seasonData" type="assocarray" alias="seasons.TVSeasonData" />
|
<field id="seasonData" type="assocarray" alias="seasons.TVSeasonData" />
|
||||||
<field id="seasonSelected" alias="seasons.rowItemSelected" alwaysNotify="true" />
|
<field id="seasonSelected" alias="seasons.rowItemSelected" alwaysNotify="true" />
|
||||||
|
<field id="quickPlayNode" type="node" alwaysNotify="true" />
|
||||||
</interface>
|
</interface>
|
||||||
</component>
|
</component>
|
|
@ -41,6 +41,7 @@ sub init()
|
||||||
m.nextEpisodeButton = m.top.findNode("nextEpisode")
|
m.nextEpisodeButton = m.top.findNode("nextEpisode")
|
||||||
m.nextEpisodeButton.text = tr("Next Episode")
|
m.nextEpisodeButton.text = tr("Next Episode")
|
||||||
m.nextEpisodeButton.setFocus(false)
|
m.nextEpisodeButton.setFocus(false)
|
||||||
|
m.nextupbuttonseconds = m.global.session.user.settings["playback.nextupbuttonseconds"].ToInt()
|
||||||
|
|
||||||
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
|
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
|
||||||
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
|
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
|
||||||
|
@ -195,17 +196,24 @@ end sub
|
||||||
'
|
'
|
||||||
' Runs Next Episode button animation and sets focus to button
|
' Runs Next Episode button animation and sets focus to button
|
||||||
sub showNextEpisodeButton()
|
sub showNextEpisodeButton()
|
||||||
if m.global.session.user.configuration.EnableNextEpisodeAutoPlay and not m.nextEpisodeButton.visible
|
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||||
|
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||||
|
|
||||||
|
if m.nextEpisodeButton.opacity = 0 and m.global.session.user.configuration.EnableNextEpisodeAutoPlay
|
||||||
|
m.nextEpisodeButton.visible = true
|
||||||
m.showNextEpisodeButtonAnimation.control = "start"
|
m.showNextEpisodeButtonAnimation.control = "start"
|
||||||
m.nextEpisodeButton.setFocus(true)
|
m.nextEpisodeButton.setFocus(true)
|
||||||
m.nextEpisodeButton.visible = true
|
|
||||||
end if
|
end if
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
'
|
'
|
||||||
'Update count down text
|
'Update count down text
|
||||||
sub updateCount()
|
sub updateCount()
|
||||||
m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.duration - m.top.position).toStr()
|
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
|
||||||
|
if nextEpisodeCountdown < 0
|
||||||
|
nextEpisodeCountdown = 0
|
||||||
|
end if
|
||||||
|
m.nextEpisodeButton.text = tr("Next Episode") + " " + nextEpisodeCountdown.toStr()
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
'
|
'
|
||||||
|
@ -218,10 +226,22 @@ end sub
|
||||||
|
|
||||||
' Checks if we need to display the Next Episode button
|
' Checks if we need to display the Next Episode button
|
||||||
sub checkTimeToDisplayNextEpisode()
|
sub checkTimeToDisplayNextEpisode()
|
||||||
if int(m.top.position) >= (m.top.duration - 30)
|
if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode"
|
||||||
showNextEpisodeButton()
|
if m.nextupbuttonseconds = 0 then return ' is the button disabled?
|
||||||
updateCount()
|
|
||||||
return
|
if isValid(m.top.duration) and isValid(m.top.position)
|
||||||
|
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
|
||||||
|
|
||||||
|
if nextEpisodeCountdown < 0 and m.nextEpisodeButton.opacity = 0.9
|
||||||
|
hideNextEpisodeButton()
|
||||||
|
return
|
||||||
|
else if nextEpisodeCountdown > 1 and int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds - 1)
|
||||||
|
updateCount()
|
||||||
|
if m.nextEpisodeButton.opacity = 0
|
||||||
|
showNextEpisodeButton()
|
||||||
|
end if
|
||||||
|
return
|
||||||
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||||
|
@ -363,8 +383,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
'Hide Next Episode Button
|
'Hide Next Episode Button
|
||||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus()
|
||||||
m.nextEpisodeButton.visible = false
|
m.nextEpisodeButton.opacity = 0
|
||||||
m.nextEpisodeButton.setFocus(false)
|
m.nextEpisodeButton.setFocus(false)
|
||||||
m.top.setFocus(true)
|
m.top.setFocus(true)
|
||||||
end if
|
end if
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
||||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
||||||
</Animation>
|
</Animation>
|
||||||
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
|
<Animation id="hideNextEpisodeButton" duration=".25" repeat="false" easeFunction="inQuad">
|
||||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
||||||
</Animation>
|
</Animation>
|
||||||
</children>
|
</children>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
Jellyfin
|
Jellyfin
|
||||||
VSCode
|
VSCode
|
||||||
|
BrighterScript
|
||||||
BrightScript
|
BrightScript
|
||||||
sideload
|
sideload
|
||||||
Sideload
|
Sideload
|
||||||
Reddit
|
Reddit
|
||||||
DEVGUIDE
|
DEVGUIDE
|
||||||
|
ImageMagick
|
||||||
ing
|
ing
|
||||||
hardcode
|
hardcode
|
||||||
Hardcoding
|
Hardcoding
|
||||||
|
@ -15,4 +17,8 @@ Repo
|
||||||
dev
|
dev
|
||||||
Dev
|
Dev
|
||||||
assignees
|
assignees
|
||||||
HTTPS
|
HTTPS
|
||||||
|
dropdown
|
||||||
|
JSDoc
|
||||||
|
JavaScript
|
||||||
|
namespaces
|
||||||
|
|
119
docs/DEVGUIDE.md
119
docs/DEVGUIDE.md
|
@ -5,25 +5,23 @@ Follow the steps below to install the app on your personal Roku device. This wil
|
||||||
- [Dev Guide For The Jellyfin Roku App](#dev-guide-for-the-jellyfin-roku-app)
|
- [Dev Guide For The Jellyfin Roku App](#dev-guide-for-the-jellyfin-roku-app)
|
||||||
- [Developer Mode](#developer-mode)
|
- [Developer Mode](#developer-mode)
|
||||||
- [Clone the GitHub Repo](#clone-the-github-repo)
|
- [Clone the GitHub Repo](#clone-the-github-repo)
|
||||||
|
- [Install Dependencies](#install-dependencies)
|
||||||
- [Method 1: Visual Studio Code](#method-1-visual-studio-code)
|
- [Method 1: Visual Studio Code](#method-1-visual-studio-code)
|
||||||
- [Install VSCode](#install-vscode)
|
- [Install VSCode](#install-vscode)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Hardcoding Roku Information](#hardcoding-roku-information)
|
- [Hardcoding Roku Information](#hardcoding-roku-information)
|
||||||
- [Method 2: Sideload to Roku Device Manually](#method-2-sideload-to-roku-device-manually)
|
- [Method 2: Command Line](#method-2-command-line)
|
||||||
- [Method 3: Direct load to Roku Device](#method-3-direct-load-to-roku-device)
|
- [Workflow](#workflow)
|
||||||
- [Login Details](#login-details)
|
- [Install Command Line Dependencies](#install-command-line-dependencies)
|
||||||
- [Install Necessary Packages](#install-necessary-packages)
|
|
||||||
- [Deploy](#deploy)
|
- [Deploy](#deploy)
|
||||||
- [Bug/Crash Reports](#bugcrash-reports)
|
- [Bug/Crash Reports](#bugcrash-reports)
|
||||||
- [Upgrade](#upgrade)
|
- [(Optional) Update Images](#optional-update-images)
|
||||||
- [Command Line Workflow](#command-line-workflow)
|
|
||||||
- [Committing](#committing)
|
- [Committing](#committing)
|
||||||
- [(Optional) Update Images](#optional-update-images)
|
|
||||||
- [Adding a User Setting](#adding-a-user-setting)
|
- [Adding a User Setting](#adding-a-user-setting)
|
||||||
- [The order of any particular menu is as follows](#the-order-of-any-particular-menu-is-as-follows)
|
- [The order of any particular menu is as follows](#the-order-of-any-particular-menu-is-as-follows)
|
||||||
- [When giving your setting a name](#when-giving-your-setting-a-name)
|
- [When giving your setting a name](#when-giving-your-setting-a-name)
|
||||||
- [When giving your setting a description](#when-giving-your-setting-a-description)
|
- [When giving your setting a description](#when-giving-your-setting-a-description)
|
||||||
- [**Remember to add all new strings to locale/en\_US/translations.ts**](#remember-to-add-all-new-strings-to-localeen_ustranslationsts)
|
- [**Remember to add all new strings to locale/en_US/translations.ts**](#remember-to-add-all-new-strings-to-localeen_ustranslationsts)
|
||||||
|
|
||||||
## Developer Mode
|
## Developer Mode
|
||||||
|
|
||||||
|
@ -43,7 +41,11 @@ Open up the new folder:
|
||||||
cd jellyfin-roku
|
cd jellyfin-roku
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Dependencies:
|
## Install Dependencies
|
||||||
|
|
||||||
|
You'll need [`npm`](https://nodejs.org), version 16 at least.
|
||||||
|
|
||||||
|
Then, use it to install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
@ -73,33 +75,36 @@ Out of the box, the BrightScript extension will prompt you to pick a Roku device
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"brightscript.debug.host": "YOUR_ROKU_HOST_HERE",
|
"brightscript.debug.host": "YOUR_ROKU_HOST_HERE",
|
||||||
"brightscript.debug.password": "YOUR_ROKU_DEV_PASSWORD_HERE",
|
"brightscript.debug.password": "YOUR_ROKU_DEV_PASSWORD_HERE"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
![image](https://user-images.githubusercontent.com/2544493/170485209-0dbe6787-8026-47e7-9095-1df96cda8a0a.png)
|
![image](https://user-images.githubusercontent.com/2544493/170485209-0dbe6787-8026-47e7-9095-1df96cda8a0a.png)
|
||||||
|
|
||||||
## Method 2: Sideload to Roku Device Manually
|
## Method 2: Command Line
|
||||||
|
|
||||||
Install Necessary Packages
|
### Workflow
|
||||||
|
|
||||||
```bash
|
Modify code -> `make build-dev install` -> Use Roku remote to test changes -> `telnet ${ROKU_DEV_TARGET} 8085` -> `CTRL + ]` -> `quit + ENTER`
|
||||||
sudo apt-get install wget make zip
|
|
||||||
```
|
You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to BrightScript or are working on tricky code.
|
||||||
|
|
||||||
|
### Install Command Line Dependencies
|
||||||
|
|
||||||
|
You'll need [`make`](https://www.gnu.org/software/make) and [`curl`](https://curl.se).
|
||||||
|
|
||||||
Build the package
|
Build the package
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev
|
make build-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create a zip in `out/apps/Jellyfin_Roku-dev.zip`. Login to your Roku's device in your browser and upload the zip file then run install.
|
This will create a zip in `out/jellyfin-roku.zip`, that you can upload on your Roku's device via your browser.
|
||||||
|
Or you can continue with the next steps to do it via the command line.
|
||||||
|
|
||||||
## Method 3: Direct load to Roku Device
|
### Deploy
|
||||||
|
|
||||||
### Login Details
|
|
||||||
|
|
||||||
Run this command - replacing the IP and password with your Roku device IP and dev password from the first step:
|
Run this command - replacing the IP and password with your Roku device IP and dev password from the first step:
|
||||||
|
|
||||||
|
@ -108,16 +113,6 @@ export ROKU_DEV_TARGET=192.168.1.234
|
||||||
export ROKU_DEV_PASSWORD=password
|
export ROKU_DEV_PASSWORD=password
|
||||||
```
|
```
|
||||||
|
|
||||||
Normally you would have to open up your browser and upload a .zip file containing the app code. These commands enable the app to be zipped up and installed on the Roku automatically which is essential for developers and makes it easy to upgrade in the future for users.
|
|
||||||
|
|
||||||
### Install Necessary Packages
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install wget make zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy
|
|
||||||
|
|
||||||
Package up the application, send it to your Roku, and launch the channel:
|
Package up the application, send it to your Roku, and launch the channel:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -126,9 +121,9 @@ make install
|
||||||
|
|
||||||
Note: You only have to run this command once if you are not a developer. The Jellyfin channel will still be installed after rebooting your Roku device.
|
Note: You only have to run this command once if you are not a developer. The Jellyfin channel will still be installed after rebooting your Roku device.
|
||||||
|
|
||||||
## Bug/Crash Reports
|
### Bug/Crash Reports
|
||||||
|
|
||||||
Did the app crash? Find a nasty bug? Use the this command to view the error log and [report it to the developers](https://github.com/jellyfin/jellyfin-roku/issues):
|
Did the app crash? Find a nasty bug? Use this command to view the error log and [report it to the developers](https://github.com/jellyfin/jellyfin-roku/issues):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
telnet ${ROKU_DEV_TARGET} 8085
|
telnet ${ROKU_DEV_TARGET} 8085
|
||||||
|
@ -136,51 +131,17 @@ telnet ${ROKU_DEV_TARGET} 8085
|
||||||
|
|
||||||
To exit telnet: `CTRL + ]` and then type `quit + ENTER`
|
To exit telnet: `CTRL + ]` and then type `quit + ENTER`
|
||||||
|
|
||||||
## Upgrade
|
You can also take a screenshot of the app to augment the bug report.
|
||||||
|
|
||||||
Navigate to the folder where you installed the app then upgrade the code to the latest version:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
make screenshot
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy the app:
|
### (Optional) Update Images
|
||||||
|
|
||||||
```bash
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command Line Workflow
|
|
||||||
|
|
||||||
Modify code -> `make install` -> Use Roku remote to test changes -> `telnet ${ROKU_DEV_TARGET} 8085` -> `CTRL + ]` -> `quit + ENTER`
|
|
||||||
|
|
||||||
Unfortunately there is no debugger. You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to BrightScript or are working on tricky code.
|
|
||||||
|
|
||||||
Install necessary packages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install nodejs npm
|
|
||||||
```
|
|
||||||
|
|
||||||
## Committing
|
|
||||||
|
|
||||||
Before committing your code, please run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make prep_commit
|
|
||||||
```
|
|
||||||
|
|
||||||
This will format your code and run the CI checks locally to ensure you will pass the CI tests.
|
|
||||||
|
|
||||||
## (Optional) Update Images
|
|
||||||
|
|
||||||
This repo already contains all necessary images for the app. This script only needs to be run when the [official Jellyfin images](https://github.com/jellyfin/jellyfin-ux) are changed to allow us to update the repo images.
|
This repo already contains all necessary images for the app. This script only needs to be run when the [official Jellyfin images](https://github.com/jellyfin/jellyfin-ux) are changed to allow us to update the repo images.
|
||||||
|
|
||||||
Install necessary packages:
|
You'll need `convert`, from [ImageMagick](https://imagemagick.org)
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install imagemagick
|
|
||||||
```
|
|
||||||
|
|
||||||
Download and convert images:
|
Download and convert images:
|
||||||
|
|
||||||
|
@ -188,9 +149,19 @@ Download and convert images:
|
||||||
make get_images
|
make get_images
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Committing
|
||||||
|
|
||||||
|
Before committing your code, please run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
And fix any encountered issue.
|
||||||
|
|
||||||
## Adding a User Setting
|
## Adding a User Setting
|
||||||
|
|
||||||
Your new functionality may need a setting to configure its behavior, or, sometimes, we may ask you to add a setting for your new functionality, so that users may enable or disable it. If you find yourself in this position, please observe the following considerations when adding your new user setting.
|
Your new functionality may need a setting to configure its behavior, or, sometimes, we may ask you to add a setting for your new functionality, so that users may enable or disable it. If you find yourself in this position, please observe the following considerations when adding your new user setting.
|
||||||
|
|
||||||
### The order of any particular menu is as follows
|
### The order of any particular menu is as follows
|
||||||
|
|
||||||
|
@ -200,7 +171,7 @@ Your new functionality may need a setting to configure its behavior, or, sometim
|
||||||
|
|
||||||
### When giving your setting a name
|
### When giving your setting a name
|
||||||
|
|
||||||
Ideally, your setting will be named with a relevant noun such as ```Cinema Mode``` or ```Codec Support.``` Sometimes there is no such name that is sufficiently specific, such as with ```Clock```. In this case you must use a verb phrase to name your setting, such as ```Hide Clock.``` If your verb phrase _must_ be long to be specific, you may drop implied verbs if absolutely necessary, such as how ```Text Subtitles Only``` drops the implied ```Show.``` Do not use the infinitive form ```action-doing``` or ```doing stuff.``` Instead, use the imperative: ```Do Action``` or ```Do Stuff.``` Remember that _characters are a commodity in names._
|
Ideally, your setting will be named with a relevant noun such as `Cinema Mode` or `Codec Support.` Sometimes there is no such name that is sufficiently specific, such as with `Clock`. In this case you must use a verb phrase to name your setting, such as `Hide Clock.` If your verb phrase _must_ be long to be specific, you may drop implied verbs if absolutely necessary, such as how `Text Subtitles Only` drops the implied `Show.` Do not use the infinitive form `action-doing` or `doing stuff.` Instead, use the imperative: `Do Action` or `Do Stuff.` Remember that _characters are a commodity in names._
|
||||||
|
|
||||||
Generally, we should not repeat the name of a setting's parent in the setting's name. Being a child of that parent implies that the settings are related to it.
|
Generally, we should not repeat the name of a setting's parent in the setting's name. Being a child of that parent implies that the settings are related to it.
|
||||||
|
|
||||||
|
|
11
docs/api-docs-readme.md
Normal file
11
docs/api-docs-readme.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Welcome
|
||||||
|
|
||||||
|
Use the `Modules` dropdown or the search feature to find files and functions to inspect
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
- BrighterScript namespaces:
|
||||||
|
- Duplicate function names will prevent the entire file from being parsed by JSDoc i.e. having `namespace.red.Delete()` and `namespace.blue.Delete()`
|
||||||
|
- When viewing source files:
|
||||||
|
- The syntax highlighter doesn't support BrightScript and will treat all source files as JavaScript.
|
||||||
|
- The page scrolls to the correct line number but it does not highlight the selected line.
|
File diff suppressed because one or more lines are too long
251
docs/api/components_ButtonGroupVert.bs.html
Normal file
251
docs/api/components_ButtonGroupVert.bs.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
307
docs/api/components_Buttons_SlideOutButton.bs.html
Normal file
307
docs/api/components_Buttons_SlideOutButton.bs.html
Normal file
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
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