Merge remote-tracking branch 'upstream/unstable' into rename-files

This commit is contained in:
Charles Ewert 2023-11-04 21:01:11 -04:00
commit 323b147401
424 changed files with 332195 additions and 13544 deletions

26
.github/release.yml vendored Normal file
View 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

View File

@ -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

View File

@ -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:

View File

@ -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
View 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>

View File

@ -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
View 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

View File

@ -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"

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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'

View File

@ -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": {

View 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

View 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>

View 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

View 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>

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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" />

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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 = {}

View 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

View 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>

View 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"
}
]
}
]
}
]
}
]
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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
View 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

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

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