Merge branch 'unstable' of https://github.com/jellyfin/jellyfin-roku into Add-Loading-ux-to-movies-details-screen

This commit is contained in:
candry7731 2023-02-01 21:12:29 -06:00
commit b61580c77a
109 changed files with 15678 additions and 10413 deletions

View File

@ -7,15 +7,19 @@ assignees: ''
---
**Software Versions**
Jellyfin Server Version:
Roku Client Version:
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
**How To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
4. Bug occurs
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
@ -26,5 +30,10 @@ assignees: ''
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Connection Information**
Is server local or remote?
Is server connection http or https?
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@ -9,7 +9,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/stale@3de2653986ebd134983c79fe2be5d45cc3d9f4e1 # tag=v6
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
with:
days-before-issue-stale: -1
days-before-issue-close: -1
@ -17,4 +17,5 @@ jobs:
close-pr-message: "This pull request has been closed because it has been inactive for 28 days. You may submit a new pull request if desired."
days-before-pr-stale: 21
days-before-pr-close: 7
exempt-draft-pr: true
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,16 +6,16 @@ on:
- 'locale/**'
jobs:
run:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: "14.12.0"
node-version: "18.13.0"
- run: npm ci
- run: npx ropm install
- run: make dev
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: Jellyfin-Roku-dev-${{ github.sha }}
path: ${{ github.workspace }}/out/staging

View File

@ -6,12 +6,12 @@ on:
jobs:
test-build-release:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: "14.12.0"
node-version: "18.13.0"
- run: npm ci
- run: npx ropm install
- run: npm run validate
@ -22,7 +22,7 @@ jobs:
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV
- name: "Find and save build_version from manifest"
run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1
with:
recursive: false
@ -36,7 +36,7 @@ jobs:
prerelease: false
title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}
files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip

View File

@ -6,12 +6,12 @@ on:
jobs:
test-build-release:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: "14.12.0"
node-version: "18.13.0"
- run: npm ci
- run: npx ropm install
- run: npm run validate
@ -22,7 +22,7 @@ jobs:
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV
- name: "Find and save build_version from manifest"
run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1
with:
recursive: false
@ -35,7 +35,7 @@ jobs:
prerelease: true
title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}
files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip

View File

@ -3,12 +3,12 @@ on: [push, pull_request]
jobs:
run:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: "14.12.0"
node-version: "18.13.0"
- run: npm ci
- run: npx ropm install
- run: npm run validate

5
.gitignore vendored
View File

@ -12,11 +12,6 @@ roku_modules
#NPM modules
node_modules/
#Rooibos generated
rooibosFunctionMap.brs
*/buildinfo.brs
logs
#Eclipse
.buildpath
.project

View File

@ -130,37 +130,12 @@ Modify code -> `make install` -> Use Roku remote to test changes -> `telnet ${RO
Unfortunately there is no debuger. 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.
### Testing
Testing is done with the [Rooibos](https://github.com/georgejecook/rooibos/) library. This works by including tests in the deployment and then looking at telnet
for the test results.
Install necessary packages:
```bash
sudo apt-get install nodejs npm
```
Install [rooibos-cli](https://github.com/georgejecook/rooibos-cli):
```bash
npm install -g rooibos-cli
```
Deploy the application with tests:
```bash
make test
```
View test results:
```bash
telnet ${ROKU_DEV_TARGET} 8085
```
To exit telnet: `CTRL + ]` and then type `quit + ENTER`
### Committing
Before commiting your code, please run:

View File

@ -1,8 +1,6 @@
#########################################################################
# Makefile Usage:
# > make test ' run all tests
# > make testFailures ' run all tests and show only failures
#
# 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
@ -12,11 +10,9 @@
##########################################################################
APPNAME = Jellyfin_Roku
VERSION = 1.4.12
ROKU_TEST_ID = 1
ROKU_TEST_WAIT_DURATION = 5
VERSION = 1.6.3
ZIP_EXCLUDE= -x rooibos/**\* -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
include app.mk
@ -29,7 +25,4 @@ beta:
release:
$(MAKE) BUILD='release' package
test: prep_staging prep_tests remove install
echo "Running tests"
deploy: prep_staging remove install

7
app.mk
View File

@ -163,13 +163,6 @@ package: prep_staging
@echo "*** packaging $(APPNAME)-$(BUILD) complete ***"
prep_tests:
@mkdir -p $(STAGINGREL)/components/tests/; \
mkdir -p $(STAGINGREL)/source/tests/; \
cp -r $(SOURCEREL)/tests/components/* $(STAGINGREL)/components/tests/;\
cp -r $(SOURCEREL)/tests/source/* $(STAGINGREL)/source/tests/;\
./node_modules/.bin/rooibos-cli i tests/.rooibosrc.json
prep_commit:
npm run format
npm ci

View File

@ -0,0 +1,13 @@
sub init()
m.top.functionName = "getNextEpisodeTask"
end sub
sub getNextEpisodeTask()
m.nextEpisodeData = api_API().shows.getepisodes(m.top.showID, {
UserId: get_setting("active_user"),
StartItemId: m.top.videoID,
Limit: 2
})
m.top.nextEpisodeData = m.nextEpisodeData
end sub

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetNextEpisodeTask" extends="Task">
<interface>
<field id="videoID" type="string" />
<field id="showID" type="string" />
<field id="nextEpisodeData" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetNextEpisodeTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -8,6 +8,9 @@ sub init()
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.itemText.translation = [0, m.itemPoster.height + 7]
m.alwaysShowTitles = get_user_setting("itemgrid.alwaysShowTitles") = "true"
@ -40,6 +43,13 @@ sub itemContentChanged()
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title
else if itemData.type = "Series"
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
end if
end if
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title

View File

@ -1,9 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GridItem" extends="Group">
<children>
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]" >
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]">
<Poster id="backdrop" width="290" height="425" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
<Poster id="itemPoster" width="290" height="425" loadDisplayMode="scaleToZoom" />
<Poster id="itemPoster" width="290" height="425" loadDisplayMode="scaleToZoom">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[201, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
<Label id="posterText" width="280" height="415" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
</maskGroup>

View File

@ -0,0 +1,68 @@
sub init()
m.itemPoster = m.top.findNode("itemPoster")
m.posterText = m.top.findNode("posterText")
m.title = m.top.findNode("title")
m.posterText.font.size = 30
m.title.font.size = 25
m.backdrop = m.top.findNode("backdrop")
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
'Parent is MarkupGrid and it's parent is the ItemGrid
m.topParent = m.top.GetParent().GetParent()
m.title.visible = false
'Get the imageDisplayMode for these grid items
if m.topParent.imageDisplayMode <> invalid
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
m.title.visible = false
if isValid(m.topParent.showItemTitles)
if LCase(m.topParent.showItemTitles) = "showalways"
m.title.visible = true
end if
end if
itemData = m.top.itemContent
if not isValid(itemData) then return
m.itemPoster.uri = itemData.PosterUrl
m.posterText.text = itemData.title
m.title.text = itemData.title
'If Poster not loaded, ensure "blue box" is shown until loaded
if m.itemPoster.loadStatus <> "ready"
m.backdrop.visible = true
m.posterText.visible = true
end if
end sub
sub focusChanged()
if m.top.itemHasFocus = true
m.title.repeatCount = -1
else
m.title.repeatCount = 0
end if
if isValid(m.topParent.showItemTitles)
if LCase(m.topParent.showItemTitles) = "showonhover"
m.title.visible = m.top.itemHasFocus
end if
end if
end sub
'Hide backdrop and text when poster loaded
sub onPosterLoadStatusChanged()
if m.itemPoster.loadStatus = "ready"
m.backdrop.visible = false
m.posterText.visible = false
end if
end sub

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GridItemSmall" extends="Group">
<children>
<Poster id="backdrop" translation="[0,15]" width="230" height="320" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
<Poster id="itemPoster" translation="[0,15]" width="230" height="320" loadDisplayMode="scaleToZoom" />
<ScrollingLabel translation="[0,340]" id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" maxWidth="230" />
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
<Label id="posterText" width="230" height="320" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="itemHasFocus" type="boolean" onChange="focusChanged" alwaysNotify="true" />
</interface>
<script type="text/brightscript" uri="GridItemSmall.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -12,6 +12,11 @@ sub init()
m.newBackdrop = m.top.findNode("backdropTransition")
m.emptyText = m.top.findNode("emptyText")
m.genreList = m.top.findNode("genrelist")
m.genreList.observeField("itemSelected", "onGenreItemSelected")
m.genreData = CreateObject("roSGNode", "ContentNode")
m.genreList.content = m.genreData
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
m.swapAnimation.observeField("state", "swapDone")
@ -74,6 +79,12 @@ sub init()
end if
end sub
'
'Genre Item Selected
sub onGenreItemSelected()
m.top.selectedItem = m.genreList.content.getChild(m.genreList.rowItemSelected[0]).getChild(m.genreList.rowItemSelected[1])
end sub
'
'Load initial set of Data
sub loadInitialItems()
@ -241,7 +252,10 @@ sub setMoviesOptions(options)
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
{ "Title": tr("Favorites"), "Name": "Favorites" }
{ "Title": tr("Favorites"), "Name": "Favorites" },
{ "Title": tr("Played"), "Name": "Played" },
{ "Title": tr("Unplayed"), "Name": "Unplayed" },
{ "Title": tr("Resumable"), "Name": "Resumable" }
]
end sub
@ -256,7 +270,9 @@ sub setBoxsetsOptions(options)
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
{ "Title": tr("Favorites"), "Name": "Favorites" }
{ "Title": tr("Favorites"), "Name": "Favorites" },
{ "Title": tr("Played"), "Name": "Played" },
{ "Title": tr("Unplayed"), "Name": "Unplayed" }
]
end sub
@ -278,8 +294,18 @@ sub setTvShowsOptions(options)
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
{ "Title": tr("Favorites"), "Name": "Favorites" }
{ "Title": tr("Favorites"), "Name": "Favorites" },
{ "Title": tr("Played"), "Name": "Played" },
{ "Title": tr("Unplayed"), "Name": "Unplayed" }
]
if isValid(m.view)
if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
options.sort = [{ "Title": tr("TITLE"), "Name": "SortName" }]
options.filter = []
end if
end if
end sub
' Set Live TV view, sort, and filter options
@ -320,13 +346,14 @@ end sub
' Set Photo Album view, sort, and filter options
sub setPhotoAlbumOptions(options)
' TODO/FIXME: Show shuffle options once implemented
' options.views = [
' { "Title": tr("Don't Shuffle"), "Name": "singlephoto"}
' { "Title": tr("Shuffle"), "Name": "shufflephoto"}
' ]
options.views = []
options.views = [
{ "Title": tr("Slideshow Off"), "Name": "singlephoto" }
{ "Title": tr("Slideshow On"), "Name": "slideshowphoto" }
{ "Title": tr("Random Off"), "Name": "singlephoto" }
{ "Title": tr("Random On"), "Name": "randomphoto" }
]
options.sort = []
options.filter = []
end sub
' Set Default view, sort, and filter options
@ -422,10 +449,32 @@ sub ItemDataLoaded(msg)
return
end if
if m.loadItemsTask.view = "Genres"
' Reset genre list data
m.genreData.removeChildren(m.genreData.getChildren(-1, 0))
for each item in itemData
m.genreData.appendChild(item)
end for
m.itemGrid.opacity = "0"
m.genreList.opacity = "1"
m.itemGrid.setFocus(false)
m.genreList.setFocus(true)
m.loading = false
m.spinner.visible = false
return
end if
for each item in itemData
m.data.appendChild(item)
end for
m.itemGrid.opacity = "1"
m.genreList.opacity = "0"
'Update the stored counts
m.loadedItems = m.itemGrid.content.getChildCount()
m.loadedRows = m.loadedItems / m.itemGrid.numColumns
@ -437,6 +486,7 @@ sub ItemDataLoaded(msg)
end if
m.itemGrid.setFocus(true)
m.genreList.setFocus(false)
m.spinner.visible = false
end sub
@ -574,14 +624,17 @@ sub optionsClosed()
end if
end if
if m.top.parentItem.Type = "CollectionFolder" or m.top.parentItem.CollectionType = "CollectionFolder"
' Did the user just request "Shuffle" on a PhotoAlbum?
if m.top.parentItem.Type = "CollectionFolder" or m.top.parentItem.Type = "Folder" or m.top.parentItem.CollectionType = "CollectionFolder"
' Did the user just request "Random" on a PhotoAlbum?
if m.options.view = "singlephoto"
' TODO/FIXME: Stop shuffling here
print "TODO/FIXME: Stop any shuffling here"
else if m.options.view = "shufflephoto"
' TODO/FIXME: Start shuffling here
print "TODO/FIXME: Start shuffle here"
set_user_setting("photos.slideshow", "false")
set_user_setting("photos.random", "false")
else if m.options.view = "slideshowphoto"
set_user_setting("photos.slideshow", "true")
set_user_setting("photos.random", "false")
else if m.options.view = "randomphoto"
set_user_setting("photos.random", "true")
set_user_setting("photos.slideshow", "false")
end if
end if
@ -641,7 +694,10 @@ sub optionsClosed()
m.itemGrid.content = m.data
loadInitialItems()
end if
m.itemGrid.setFocus(true)
m.itemGrid.setFocus(m.itemGrid.opacity = 1)
m.genreList.setFocus(m.genreList.opacity = 1)
if m.tvGuide <> invalid
m.tvGuide.lastFocus.setFocus(true)
end if
@ -677,13 +733,19 @@ end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
topGrp = m.top.findNode("itemGrid")
if m.itemGrid.opacity = 1
topGrp = m.itemGrid
else
topGrp = m.genreList
end if
searchGrp = m.top.findNode("voiceBox")
if key = "left" and searchGrp.isinFocusChain()
topGrp.setFocus(true)
searchGrp.setFocus(false)
end if
if key = "options"
if m.options.visible = true
m.options.visible = false
@ -724,9 +786,10 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
else if itemToPlay <> invalid and itemToPlay.type = "Photo"
' Spawn photo player task
photoPlayer = CreateObject("roSgNode", "PhotoPlayerTask")
photoPlayer.itemContent = itemToPlay
photoPlayer.control = "RUN"
photoPlayer = CreateObject("roSgNode", "PhotoDetails")
photoPlayer.items = markupGrid
photoPlayer.itemIndex = markupGrid.itemFocused
m.global.sceneManager.callfunc("pushScene", photoPlayer)
return true
end if
else if key = "left" and topGrp.isinFocusChain()
@ -768,14 +831,16 @@ function onKeyEvent(key as string, press as boolean) as boolean
end function
sub updateTitle()
if m.filter = "All"
m.top.overhangTitle = m.top.parentItem.title
else if m.filter = "Favorites"
m.top.overhangTitle = m.top.parentItem.title
if m.filter = "Favorites"
m.top.overhangTitle = m.top.parentItem.title + " " + tr("(Favorites)")
end if
if m.voiceBox.text <> ""
m.top.overhangTitle = m.top.parentItem.title + tr(" (Filtered by ") + m.loadItemsTask.searchTerm + ")"
end if
if m.top.alphaSelected <> ""
m.top.overhangTitle = m.top.parentItem.title + tr(" (Filtered by ") + m.loadItemsTask.nameStartsWith + ")"
end if
@ -789,14 +854,18 @@ sub updateTitle()
if m.options.view = "Networks" or m.view = "Networks"
m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Networks"))
end if
if m.options.view = "Studios" or m.view = "Studios"
m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Studios"))
end if
if m.options.view = "Genres" or m.view = "Genres"
m.top.overhangTitle = "%s (%s)".Format(m.top.parentItem.title, tr("Genres"))
end if
actInt = m.itemGrid.itemFocused + 1
if m.showItemCount and m.loadItemsTask.totalRecordCount > 0
if m.showItemCount and m.loadItemsTask.totalRecordCount > 0 and m.options.view <> "Genres" and m.view <> "Genres"
m.top.overhangTitle += " (" + tr("%1 of %2").Replace("%1", actInt.toStr()).Replace("%2", m.loadItemsTask.totalRecordCount.toStr()) + ")"
end if

View File

@ -3,18 +3,8 @@
<children>
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x262626ff" translation = "[25, 75]" />
<poster id="backdrop"
loadDisplayMode="scaleToFill"
width="1920"
height="1080"
opacity="0.25"
/>
<poster id="backdropTransition"
loadDisplayMode="scaleToFill"
width="1920"
height="1080"
opacity="0.25"
/>
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1920" height="1080" opacity="0.25" />
<poster id="backdropTransition" loadDisplayMode="scaleToFill" width="1920" height="1080" opacity="0.25" />
<MarkupGrid
id = "itemGrid"
translation = "[ 96, 160 ]"
@ -25,14 +15,17 @@
itemSize = "[ 290, 425 ]"
itemSpacing = "[ 0, 45 ]"
drawFocusFeedback = "false" />
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
<RowList opacity="0" id="genrelist" translation="[120, 160]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize = "[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear" >
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="backdrop.opacity" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="backdrop.opacity" />
</Animation>
<Alpha id="AlphaMenu" />
</children>

View File

@ -20,6 +20,16 @@ sub loadItems()
sort_order = "Descending"
end if
if m.top.ItemType = "LogoImage"
logoImageExists = api_API().items.headimageurlbyname(m.top.itemId, "logo")
if logoImageExists
m.top.content = [api_API().items.getimageurl(m.top.itemId, "logo", 0, { "maxHeight": 500, "maxWidth": 500, "quality": "90" })]
else
m.top.content = []
end if
return
end if
params = {
limit: m.top.limit,
@ -52,16 +62,25 @@ sub loadItems()
end if
end if
if m.top.searchTerm <> ""
'reset data
if LCase(m.top.searchTerm) = LCase(tr("all"))
params.searchTerm = " "
else if m.top.searchTerm <> ""
params.searchTerm = m.top.searchTerm
end if
filter = m.top.filter
if filter = "All" or filter = "all"
filter = LCase(m.top.filter)
if filter = "all"
' do nothing
else if filter = "Favorites"
else if filter = "favorites"
params.append({ Filters: "IsFavorite" })
params.append({ isFavorite: true })
else if filter = "unplayed"
params.append({ Filters: "IsUnplayed" })
else if filter = "played"
params.append({ Filters: "IsPlayed" })
else if filter = "resumable"
params.append({ Filters: "IsResumable" })
end if
if m.top.ItemType <> ""
@ -76,13 +95,14 @@ sub loadItems()
params.append({ UserId: get_setting("active_user") })
else if m.top.view = "Genres"
url = "Genres"
params.append({ UserId: get_setting("active_user") })
params.append({ UserId: get_setting("active_user"), includeItemTypes: m.top.itemType })
else if m.top.ItemType = "MusicArtist"
url = "Artists"
params.append({
UserId: get_setting("active_user")
UserId: get_setting("active_user"),
Fields: "Genres"
})
params.IncludeItemTypes = ""
params.IncludeItemTypes = "MusicAlbum,Audio"
else if m.top.ItemType = "MusicAlbum"
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
params.append({ ImageTypeLimit: 1 })
@ -90,6 +110,7 @@ sub loadItems()
else
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
end if
resp = APIRequest(url, params)
data = getJson(resp)
if data <> invalid
@ -108,7 +129,7 @@ sub loadItems()
tmp = CreateObject("roSGNode", "ChannelData")
else if item.Type = "Folder" or item.Type = "ChannelFolderItem" or item.Type = "CollectionFolder"
tmp = CreateObject("roSGNode", "FolderData")
else if item.Type = "Video"
else if item.Type = "Video" or item.Type = "Recording"
tmp = CreateObject("roSGNode", "VideoData")
else if item.Type = "Photo"
tmp = CreateObject("roSGNode", "PhotoData")
@ -117,7 +138,61 @@ sub loadItems()
else if item.type = "Episode"
tmp = CreateObject("roSGNode", "TVEpisode")
else if item.Type = "Genre"
tmp = CreateObject("roSGNode", "FolderData")
tmp = CreateObject("roSGNode", "ContentNode")
tmp.title = item.name
genreData = api_API().users.getitemsbyquery(get_setting("active_user"), {
SortBy: "Random",
SortOrder: "Ascending",
IncludeItemTypes: m.top.itemType,
Recursive: true,
Fields: "PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo",
ImageTypeLimit: 1,
EnableImageTypes: "Primary",
Limit: 6,
GenreIds: item.id,
EnableTotalRecordCount: false,
ParentId: m.top.itemId
})
if genreData.Items.Count() > 5
' Add View All item to the start of the row
row = tmp.createChild("FolderData")
row.parentFolder = m.top.itemId
row.title = tr("View All") + " " + item.name
item.name = tr("View All") + " " + item.name
row.json = item
row.type = "Folder"
if LCase(m.top.itemType) = "movie"
genreItemImage = api_API().items.getimageurl(item.id)
else
genreItemImage = invalid
row.posterURL = invalid
end if
row.FHDPOSTERURL = genreItemImage
row.HDPOSTERURL = genreItemImage
row.SDPOSTERURL = genreItemImage
end if
for each genreItem in genreData.Items
if LCase(m.top.itemType) = "movie"
row = tmp.createChild("MovieData")
else
row = tmp.createChild("SeriesData")
end if
genreItemImage = api_API().items.getimageurl(genreItem.id)
row.title = genreItem.name
row.FHDPOSTERURL = genreItemImage
row.HDPOSTERURL = genreItemImage
row.SDPOSTERURL = genreItemImage
row.json = genreItem
row.id = genreItem.id
row.type = genreItem.type
end for
else if item.Type = "Studio"
tmp = CreateObject("roSGNode", "FolderData")
else if item.Type = "MusicAlbum"
@ -132,15 +207,27 @@ sub loadItems()
tmp = CreateObject("roSGNode", "MusicArtistData")
else if item.Type = "Audio"
tmp = CreateObject("roSGNode", "MusicSongData")
else if item.Type = "MusicGenre"
tmp = CreateObject("roSGNode", "FolderData")
tmp.title = item.name
tmp.parentFolder = m.top.itemId
tmp.json = item
tmp.type = "Folder"
tmp.posterUrl = api_API().items.getimageurl(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
else
print "[LoadItems] Unknown Type: " item.Type
end if
if tmp <> invalid
tmp.parentFolder = m.top.itemId
tmp.json = item
if item.UserData <> invalid and item.UserData.isFavorite <> invalid
tmp.favorite = item.UserData.isFavorite
if item.Type <> "Genre" and item.Type <> "MusicGenre"
tmp.parentFolder = m.top.itemId
tmp.json = item
if item.UserData <> invalid and item.UserData.isFavorite <> invalid
tmp.favorite = item.UserData.isFavorite
end if
end if
results.push(tmp)
end if
end for

View File

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

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="MovieLibraryView" extends="JFScreen">
<children>
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" />
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation = "[25, 75]" />
<maskGroup translation="[820, 0]" id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" maskSize="[1220,700]">
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1100" height="700" opacity="1" />
<poster id="backdropTransition" loadDisplayMode="scaleToFill" width="1100" height="700" opacity="1" />
</maskGroup>
<Label id="selectedMovieName" visible="false" translation="[120, 40]" wrap="true" font="font:LargeBoldSystemFont" width="850" height="196" horizAlign="left" vertAlign="center" />
<Poster id="movieLogo" visible="false" translation="[120, 40]" loadDisplayMode="scaleToFit" width="384" height="196" />
<LayoutGroup layoutDirection="horiz" translation="[120, 270]" itemSpacings="[30]" id="infoGroup">
<Label id="selectedMovieProductionYear" font="font:SmallestSystemFont" />
<Label id="runtime" font="font:SmallestSystemFont" />
<Label id="selectedMovieOfficialRating" font="font:SmallestSystemFont" />
<LayoutGroup id="communityRatingGroup" visible="false" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="28" width="28" blendColor="#00a4dcFF" />
<Label id="communityRating" font="font:SmallestSystemFont" />
</LayoutGroup>
<LayoutGroup layoutDirection="horiz" id="criticRatingGroup">
<Poster id="criticRatingIcon" height="28" width="28" />
<Label id="criticRatingLabel" font="font:SmallestSystemFont" />
</LayoutGroup>
</LayoutGroup>
<Label id="selectedMovieOverview" font="font:SmallestSystemFont" translation="[120, 360]" wrap="true" lineSpacing="20" maxLines="5" width="850" ellipsisText="..." />
<MarkupGrid id="itemGrid" itemComponentName="GridItemSmall" numColumns="7" numRows="2" vertFocusAnimationStyle="fixed" itemSize="[230, 310]" itemSpacing="[20, 20]" />
<RowList opacity="0" id="genrelist" translation="[120, 60]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize = "[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
</Animation>
<Alpha id="AlphaMenu" />
</children>
<interface>
<field id="HomeLibraryItem" type="string"/>
<field id="parentItem" type="node" onChange="loadInitialItems" />
<field id="selectedItem" type="node" alwaysNotify="true" />
<field id="quickPlayNode" type="node" alwaysNotify="true" />
<field id="imageDisplayMode" type="string" value="scaleToZoom" />
<field id="AlphaSelected" type="string" alias="AlphaMenu.itemAlphaSelected" alwaysNotify="true" onChange="onItemAlphaSelected" />
<field id="alphaActive" type="boolean" value="false" />
<field id="showItemTitles" type="string" value="showonhover" />
<field id="jumpToItem" type="integer" value="" />
</interface>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="MovieLibraryView.brs" />
</component>

View File

@ -0,0 +1,48 @@
sub init()
m.itemPoster = m.top.findNode("itemPoster")
m.posterText = m.top.findNode("posterText")
m.posterText.font.size = 30
m.backdrop = m.top.findNode("backdrop")
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
'Parent is MarkupGrid and it's parent is the ItemGrid
m.topParent = m.top.GetParent().GetParent()
'Get the imageDisplayMode for these grid items
if m.topParent.imageDisplayMode <> invalid
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
itemData = m.top.itemContent
if not isValid(itemData) then return
if LCase(itemData.type) = "musicalbum"
m.backdrop.uri = "pkg:/images/icons/album.png"
else if LCase(itemData.type) = "musicartist"
m.backdrop.uri = "pkg:/images/missingArtist.png"
else if LCase(itemData.json.type) = "musicgenre"
m.backdrop.uri = "pkg:/images/icons/musicFolder.png"
end if
m.itemPoster.uri = itemData.PosterUrl
m.posterText.text = itemData.title
'If Poster not loaded, ensure "blue box" is shown until loaded
if m.itemPoster.loadStatus <> "ready"
m.backdrop.visible = true
end if
end sub
'Hide backdrop and text when poster loaded
sub onPosterLoadStatusChanged()
if m.itemPoster.loadStatus = "ready"
m.backdrop.visible = false
end if
end sub

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="MusicArtistGridItem" extends="Group">
<children>
<Poster id="backdrop" translation="[0,15]" width="280" height="280" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
<Poster id="itemPoster" translation="[0,15]" width="280" height="280" loadDisplayMode="scaleToZoom" />
<Rectangle id="postTextBackground" height="50" width="270" color="0x000000DD" translation = "[5, 240]">
<ScrollingLabel id="posterText" color="#FFFFFF" maxWidth="270" height="50" horizAlign="center" vertAlign="center" />
</Rectangle>
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
</interface>
<script type="text/brightscript" uri="MusicArtistGridItem.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

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

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="MusicLibraryView" extends="JFScreen">
<children>
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" />
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation = "[25, 75]" />
<maskGroup translation="[820, 0]" id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" maskSize="[1220,445]">
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1100" height="450" opacity="1" />
<poster id="backdropTransition" loadDisplayMode="scaleToFill" width="1100" height="450" opacity="1" />
</maskGroup>
<Label id="selectedArtistName" visible="false" translation="[120, 40]" wrap="true" font="font:LargeBoldSystemFont" width="850" height="196" horizAlign="left" vertAlign="center" />
<Poster id="artistLogo" visible="false" translation="[120, 40]" loadDisplayMode="scaleToFit" width="384" height="196" />
<Label id="selectedArtistSongCount" translation="[120, 270]" wrap="true" font="font:SmallestSystemFont" width="850" height="30" horizAlign="left" />
<Label id="selectedArtistAlbumCount" translation="[120, 310]" wrap="true" font="font:SmallestSystemFont" width="850" height="30" horizAlign="left" />
<Label id="selectedArtistGenres" translation="[120, 350]" wrap="true" font="font:SmallestSystemFont" width="850" height="30" horizAlign="left" />
<MarkupGrid id="itemGrid" itemComponentName="MusicArtistGridItem" numColumns="6" numRows="2" vertFocusAnimationStyle="fixed" itemSize="[280, 280]" itemSpacing="[20, 20]" />
<MarkupGrid id="genrelist" itemComponentName="MusicArtistGridItem" numColumns="6" numRows="4" vertFocusAnimationStyle="fixed" translation="[96, 60]" itemSize="[280, 280]" itemSpacing="[20, 20]" opacity="0" />
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
</Animation>
<Alpha id="AlphaMenu" />
</children>
<interface>
<field id="HomeLibraryItem" type="string"/>
<field id="View" type="string"/>
<field id="parentItem" type="node" onChange="loadInitialItems" />
<field id="selectedItem" type="node" alwaysNotify="true" />
<field id="quickPlayNode" type="node" alwaysNotify="true" />
<field id="imageDisplayMode" type="string" value="scaleToZoom" />
<field id="AlphaSelected" type="string" alias="AlphaMenu.itemAlphaSelected" alwaysNotify="true" onChange="onItemAlphaSelected" />
<field id="alphaActive" type="boolean" value="false" />
<field id="jumpToItem" type="integer" value="" />
</interface>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="MusicLibraryView.brs" />
</component>

View File

@ -25,19 +25,14 @@ sub init()
' get system preference clock format (12/24hr)
di = CreateObject("roDeviceInfo")
m.clockFormat = di.GetClockFormat()
' grab current time
currentTime = CreateObject("roDateTime")
currentTime.ToLocalTime()
m.currentHours = currentTime.GetHours()
m.currentMinutes = currentTime.GetMinutes()
m.overlayHours = m.top.findNode("overlayHours")
m.overlayMinutes = m.top.findNode("overlayMinutes")
m.overlayMeridian = m.top.findNode("overlayMeridian")
' start timer
m.currentTimeTimer = m.top.findNode("currentTimeTimer")
m.currentTimeTimer.control = "start"
m.currentTimeTimer.ObserveField("fire", "updateTime")
updateTimeDisplay()
end if
setClockVisibility()
@ -97,64 +92,50 @@ sub updateUser()
end sub
sub updateTime()
if (m.currentMinutes + 1) > 59
m.currentHours = m.currentHours + 1
m.currentMinutes = 0
else
m.currentMinutes = m.currentMinutes + 1
end if
m.currentTime = CreateObject("roDateTime")
m.currentTime.ToLocalTime()
m.currentTimeTimer.duration = 60 - m.currentTime.GetSeconds()
m.currentHours = m.currentTime.GetHours()
m.currentMinutes = m.currentTime.GetMinutes()
updateTimeDisplay()
end sub
sub resetTime()
m.currentTimeTimer.control = "stop"
currentTime = CreateObject("roDateTime")
m.currentTimeTimer.control = "start"
currentTime.ToLocalTime()
m.currentHours = currentTime.GetHours()
m.currentMinutes = currentTime.GetMinutes()
updateTimeDisplay()
updateTime()
end sub
sub updateTimeDisplay()
overlayHours = m.top.findNode("overlayHours")
overlayMinutes = m.top.findNode("overlayMinutes")
overlayMeridian = m.top.findNode("overlayMeridian")
if m.clockFormat = "24h"
overlayMeridian.text = ""
m.overlayMeridian.text = ""
if m.currentHours < 10
overlayHours.text = "0" + StrI(m.currentHours).trim()
m.overlayHours.text = "0" + StrI(m.currentHours).trim()
else
overlayHours.text = m.currentHours
m.overlayHours.text = m.currentHours
end if
else
if m.currentHours < 12
overlayMeridian.text = "AM"
m.overlayMeridian.text = "AM"
if m.currentHours = 0
overlayHours.text = "12"
m.overlayHours.text = "12"
else
overlayHours.text = m.currentHours
m.overlayHours.text = m.currentHours
end if
else
overlayMeridian.text = "PM"
m.overlayMeridian.text = "PM"
if m.currentHours = 12
overlayHours.text = "12"
m.overlayHours.text = "12"
else
overlayHours.text = m.currentHours - 12
m.overlayHours.text = m.currentHours - 12
end if
end if
end if
if m.currentMinutes < 10
overlayMinutes.text = "0" + StrI(m.currentMinutes).trim()
m.overlayMinutes.text = "0" + StrI(m.currentMinutes).trim()
else
overlayMinutes.text = m.currentMinutes
m.overlayMinutes.text = m.currentMinutes
end if
end sub

View File

@ -11,7 +11,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
else if key = "options"
group = m.global.sceneManager.callFunc("getActiveScene")
if group <> invalid and group.optionsAvailable
if isValid(group) and isValid(group.optionsAvailable) and group.optionsAvailable
group.lastFocus = group.focusedChild
panel = group.findNode("options")
panel.visible = true

View File

@ -8,4 +8,5 @@
<field id="exit" type="boolean" alwaysNotify="true" />
</interface>
<script type="text/brightscript" uri="JFScene.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -2,6 +2,8 @@ sub init()
m.playbackTimer = m.top.findNode("playbackTimer")
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")
m.playbackTimer.observeField("fire", "ReportPlayback")
m.bufferPercentage = 0 ' Track whether content is being loaded
m.playReported = false
@ -12,6 +14,88 @@ sub init()
clockNode = findNodeBySubtype(m.top, "clock")
if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node)
end if
'Play Next Episode button
m.nextEpisodeButton = m.top.findNode("nextEpisode")
m.nextEpisodeButton.text = tr("Next Episode")
m.nextEpisodeButton.setFocus(false)
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
m.checkedForNextEpisode = false
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
end sub
' Event handler for when video content field changes
sub onContentChange()
if not isValid(m.top.content) then return
m.top.observeField("position", "onPositionChanged")
' If video content type is not episode, remove position observer
if m.top.content.contenttype <> 4
m.top.unobserveField("position")
end if
end sub
sub onNextEpisodeDataLoaded()
m.checkedForNextEpisode = true
m.top.observeField("position", "onPositionChanged")
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
m.top.unobserveField("position")
end if
end sub
'
' Runs Next Episode button animation and sets focus to button
sub showNextEpisodeButton()
if not m.nextEpisodeButton.visible
m.showNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(true)
m.nextEpisodeButton.visible = true
end if
end sub
'
'Update count down text
sub updateCount()
m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.runTime - m.top.position).toStr()
end sub
'
' Runs hide Next Episode button animation and sets focus back to video
sub hideNextEpisodeButton()
m.hideNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end sub
' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
if int(m.top.position) >= (m.top.runTime - 30)
showNextEpisodeButton()
updateCount()
return
end if
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
end if
end sub
' When Video Player state changes
sub onPositionChanged()
' Check if dialog is open
m.dialog = m.top.getScene().findNode("dialogBackground")
if not isValid(m.dialog)
checkTimeToDisplayNextEpisode()
end if
end sub
'
@ -40,6 +124,16 @@ sub onState(msg)
m.top.control = "stop"
m.top.backPressed = true
else if m.top.state = "playing"
' Check if next episde is available
if isValid(m.top.showID)
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
m.getNextEpisodeTask.showID = m.top.showID
m.getNextEpisodeTask.videoID = m.top.id
m.getNextEpisodeTask.control = "RUN"
end if
end if
if m.playReported = false
ReportPlayback("start")
m.playReported = true
@ -126,17 +220,39 @@ sub dialogClosed(msg)
sourceNode.close = true
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
m.top.state = "finished"
hideNextEpisodeButton()
return true
else
'Hide Next Episode Button
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end if
end if
if not press then return false
if m.top.Subtitles.count() and key = "down"
if key = "down"
m.top.selectSubtitlePressed = true
return true
else if key = "up"
m.top.selectPlaybackInfoPressed = true
return true
else if key = "OK"
' OK will play/pause depending on current state
' return false to allow selection during seeking
if m.top.state = "paused"
m.top.control = "resume"
return false
else if m.top.state = "playing"
m.top.control = "pause"
return false
end if
end if
return false

View File

@ -22,12 +22,24 @@
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />
</interface>
<script type="text/brightscript" uri="JFVideo.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<children>
<timer id="playbackTimer" repeat="true" duration="30" />
<timer id="bufferCheckTimer" repeat="true" />
<JFButton id="nextEpisode" opacity="0" textColor="#f0f0f0" focusedTextColor="#202020" focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png" focusBitmapUri="pkg:/images/white.9.png" translation="[1500, 900]" />
<!--animation for the play next episode button-->
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
</Animation>
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
</Animation>
</children>
</component>
</component>

View File

@ -3,6 +3,8 @@ sub init()
m.staticTitle = m.top.findNode("staticTitle")
m.series = m.top.findNode("Series")
m.poster = m.top.findNode("poster")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.backdrop = m.top.findNode("backdrop")
@ -55,6 +57,13 @@ sub itemContentChanged() as void
itemData = m.top.itemContent
m.title.text = itemData.title
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
end if
end if
if itemData.json.lookup("Type") = "Episode" and itemData.json.IndexNumber <> invalid
m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text

View File

@ -2,32 +2,20 @@
<component name="ListPoster" extends="Group">
<children>
<Rectangle id="backdrop" />
<ScrollingLabel id="Series"
horizAlign="center"
font="font:SmallSystemFont"
repeatCount="0"
visible="false"
/>
<Poster id="poster" translation="[2,0]" loadDisplayMode="scaleToFit" />
<ScrollingLabel id="title"
horizAlign="center"
font="font:SmallSystemFont"
repeatCount="0"
visible="false"
/>
<Label id="staticTitle"
horizAlign="center"
font="font:SmallSystemFont"
wrap="false"
/>
<ScrollingLabel id="Series" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
<Poster id="poster" translation="[2,0]" loadDisplayMode="scaleToFit">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[104, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<ScrollingLabel id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
<Label id="staticTitle" horizAlign="center" font="font:SmallSystemFont" wrap="false" />
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged"/>
<field id="itemWidth" type="integer" />
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
<!-- mediatype -->
</interface>
<script type="text/brightscript" uri="ListPoster.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -0,0 +1,46 @@
sub init()
m.content = m.top.findNode("content")
setPalette()
m.top.id = "OKDialog"
m.top.height = 900
m.top.title = "What's New?"
m.top.buttons = [tr("OK")]
dialogStyles = {
"default": {
"fontSize": 27,
"fontUri": "font:SystemFontFile",
"color": "#EFEFEFFF"
},
"author": {
"fontSize": 27,
"fontUri": "font:SystemFontFile",
"color": "#00a4dcFF"
}
}
whatsNewList = ParseJSON(ReadAsciiFile("pkg:/source/static/whatsNew.json"))
for each item in whatsNewList
textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")
textLine.drawingStyles = dialogStyles
textLine.text = "• " + item.description + " <author>" + item.author + "</author>"
end for
end sub
sub setPalette()
dlgPalette = createObject("roSGNode", "RSGPalette")
dlgPalette.colors = {
DialogBackgroundColor: "0x262828FF",
DialogFocusColor: "0xcececeFF",
DialogFocusItemColor: "0x202020FF",
DialogSecondaryTextColor: "0xf8f8f8ff",
DialogSecondaryItemColor: "#00a4dcFF",
DialogTextColor: "0xeeeeeeFF"
}
m.top.palette = dlgPalette
end sub

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="WhatsNewDialog" extends="StandardMessageDialog">
<children>
<StdDlgContentArea id="content" />
</children>
<script type="text/brightscript" uri="WhatsNewDialog.brs" />
</component>

View File

@ -11,10 +11,10 @@ sub setPoster()
if m.top.image <> invalid
m.top.posterURL = m.top.image.url
else if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 60 }
imgParams = { "maxHeight": 60, "Tag": m.top.json.ImageTags.Primary }
m.top.hdsmalliconurl = ImageURL(m.top.json.id, "Primary", imgParams)
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
end if
end sub

View File

@ -18,16 +18,16 @@ sub setPoster()
else
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
else if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 440 }
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if
' Add Backdrop Image
if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 }
imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if

View File

@ -21,7 +21,7 @@ sub setPoster()
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
else if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
end if
end sub

View File

@ -16,7 +16,7 @@ sub setData()
' Set appropriate Images for Wide and Tall based on type
if datum.type = "CollectionFolder" or datum.type = "UserView"
params = { "maxHeight": 261, "maxWidth": 464 }
params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
m.top.widePosterUrl = m.top.thumbnailURL
@ -30,21 +30,25 @@ sub setData()
else if datum.type = "Episode"
imgParams = { "AddPlayedIndicator": datum.UserData.Played }
if datum.UserData.PlayedPercentage <> invalid
imgParams.Append({ "PercentPlayed": datum.UserData.PlayedPercentage })
end if
imgParams.Append({ "maxHeight": 261 })
imgParams.Append({ "maxWidth": 464 })
if datum.ImageTags.Primary <> invalid
param = { "Tag": datum.ImageTags.Primary }
imgParams.Append(param)
end if
m.top.thumbnailURL = ImageURL(datum.id, "Primary", imgParams)
' Add Wide Poster (Series Backdrop)
if datum.ParentThumbImageTag <> invalid
imgParams["Tag"] = datum.ParentThumbImageTag
m.top.widePosterUrl = ImageURL(datum.ParentThumbItemId, "Thumb", imgParams)
else if datum.ParentBackdropImageTags <> invalid
imgParams["Tag"] = datum.ParentBackdropImageTags[0]
m.top.widePosterUrl = ImageURL(datum.ParentBackdropItemId, "Backdrop", imgParams)
else if datum.ImageTags.Primary <> invalid
imgParams["Tag"] = datum.SeriesPrimaryImageTag
m.top.widePosterUrl = ImageURL(datum.id, "Primary", imgParams)
end if
@ -52,68 +56,72 @@ sub setData()
imgParams = { "maxHeight": 261 }
imgParams.Append({ "maxWidth": 464 })
if datum.UserData.UnplayedItemCount > 0
imgParams["UnplayedCount"] = datum.UserData.UnplayedItemCount
end if
m.top.posterURL = ImageURL(datum.id, "Primary", imgParams)
' Add Wide Poster (Series Backdrop)
if datum.ImageTags <> invalid and datum.imageTags.Thumb <> invalid
imgParams["Tag"] = datum.imageTags.Thumb
m.top.widePosterUrl = ImageURL(datum.Id, "Thumb", imgParams)
else if datum.BackdropImageTags <> invalid
imgParams["Tag"] = datum.BackdropImageTags[0]
m.top.widePosterUrl = ImageURL(datum.Id, "Backdrop", imgParams)
end if
else if datum.type = "Movie"
imgParams = { AddPlayedIndicator: datum.UserData.Played }
if datum.UserData.PlayedPercentage <> invalid
imgParams.Append({ "PercentPlayed": datum.UserData.PlayedPercentage })
end if
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"
imgParams = { AddPlayedIndicator: datum.UserData.Played }
if datum.UserData.PlayedPercentage <> invalid
imgParams.Append({ "PercentPlayed": datum.UserData.PlayedPercentage })
end if
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 = "MusicAlbum"
params = { "maxHeight": 261, "maxWidth": 261 }
params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 261 }
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
m.top.widePosterUrl = m.top.thumbnailURL
m.top.posterUrl = m.top.thumbnailURL
else if datum.type = "TvChannel" or datum.type = "Channel"
params = { "maxHeight": 261, "maxWidth": 464 }
params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 }
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
m.top.widePosterUrl = m.top.thumbnailURL
m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"

View File

@ -11,10 +11,11 @@
<field id="json" type="assocarray" onChange="setData" />
<field id="collectionType" type="string" />
<field id="imageWidth" type="integer" value="464" />
<field id="PlayedPercentage" type="float" value="0" />
<field id="usePoster" type="bool" value="false" />
</interface>
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="HomeData.brs" />
<script type="text/brightscript" uri="HomeData.brs" />
</component>

View File

@ -8,11 +8,13 @@ sub setFields()
m.top.watched = json.UserData.played
m.top.Type = "Movie"
if json.MediaSourceCount <> invalid and json.MediaSourceCount > 1
m.top.mediaSources = []
for each source in json.MediaSources
m.top.mediaSources.push(source)
end for
if isValid(json.MediaSourceCount) and json.MediaSourceCount > 1
if isValid(json.MediaSources)
m.top.mediaSources = []
for each source in json.MediaSources
m.top.mediaSources.push(source)
end for
end if
end if
if json.ProductionYear <> invalid
@ -38,20 +40,22 @@ sub setPoster()
else
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440 }
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
end if
' Add Backdrop Image
if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
if m.top.json.BackdropImageTags <> invalid
if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if
end if
end if

View File

@ -12,4 +12,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -12,19 +12,19 @@ sub setPoster()
else
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440 }
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
end if
' Add Backdrop Image
if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 }
imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if

View File

@ -14,19 +14,19 @@ sub setPoster()
else
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440 }
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag }
m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams)
end if
' Add Backdrop Image
if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 }
imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if

View File

@ -146,7 +146,13 @@ end sub
'
' Clear previous scene from group stack
sub clearPreviousScene()
m.groups.Delete(2)
m.groups.pop()
end sub
'
' Delete scene from group stack at passed index
sub deleteSceneAtIndex(index = 1)
m.groups.Delete(index)
end sub
'

View File

@ -36,7 +36,7 @@ sub setPoster()
m.top.posterURL = m.top.image.url
else
if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Thumb <> invalid
imgParams = { "maxHeight": 500, "maxWidth": 500 }
imgParams = { "maxHeight": 500, "maxWidth": 500, "Tag": m.top.json.ImageTags.Thumb }
m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
end if
end if

View File

@ -32,16 +32,16 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
else if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 440 }
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if
' Add Backdrop Image
if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 }
imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if

View File

@ -15,7 +15,7 @@ sub setPoster()
if m.top.image <> invalid
m.top.posterURL = m.top.image.url
else if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
end if
end sub

View File

@ -14,6 +14,9 @@ sub init()
m.SpecialFeaturesTask = CreateObject("roSGNode", "LoadItemsTask")
m.SpecialFeaturesTask.itemsToLoad = "specialfeatures"
m.SpecialFeaturesTask.observeField("content", "onSpecialFeaturesLoaded")
m.LoadAdditionalPartsTask = CreateObject("roSGNode", "LoadItemsTask")
m.LoadAdditionalPartsTask.itemsToLoad = "additionalparts"
m.LoadAdditionalPartsTask.observeField("content", "onAdditionalPartsLoaded")
m.LoadMoviesTask = CreateObject("roSGNode", "LoadItemsTask")
m.LoadMoviesTask.itemsToLoad = "personMovies"
m.LoadShowsTask = CreateObject("roSGNode", "LoadItemsTask")
@ -28,10 +31,11 @@ sub updateSize()
m.top.rowItemSpacing = [36, 36]
end sub
sub loadPeople(data as object)
sub loadParts(data as object)
m.top.parentId = data.id
m.LoadPeopleTask.peopleList = data.People
m.LoadPeopleTask.control = "RUN"
m.people = data.People
m.LoadAdditionalPartsTask.itemId = m.top.parentId
m.LoadAdditionalPartsTask.control = "RUN"
end sub
sub loadPersonVideos(personId)
@ -41,12 +45,32 @@ sub loadPersonVideos(personId)
m.LoadMoviesTask.control = "RUN"
end sub
sub onAdditionalPartsLoaded()
parts = m.LoadAdditionalPartsTask.content
m.LoadAdditionalPartsTask.unobserveField("content")
data = CreateObject("roSGNode", "ContentNode") ' The row Node
m.top.content = data
if parts <> invalid and parts.count() > 0
row = buildRow("Additional Parts", parts, 464)
addRowSize([464, 291])
m.top.content.appendChild(row)
m.top.rowItemSize = [[464, 291]]
else
m.top.rowItemSize = [[234, 396]]
end if
m.top.translation = "[75,10]"
' Load Cast and Crew and everything else...
m.LoadPeopleTask.peopleList = m.people
m.LoadPeopleTask.control = "RUN"
end sub
sub onPeopleLoaded()
people = m.LoadPeopleTask.content
m.loadPeopleTask.unobserveField("content")
data = CreateObject("roSGNode", "ContentNode") ' The row Node
if people <> invalid and people.count() > 0
row = data.createChild("ContentNode")
row = m.top.content.createChild("ContentNode")
row.Title = tr("Cast & Crew")
for each person in people
if person.json.type = "Actor" and person.json.Role <> invalid
@ -58,9 +82,6 @@ sub onPeopleLoaded()
row.appendChild(person)
end for
end if
m.top.content = data
m.top.translation = "[75,10]"
m.top.rowItemSize = [[234, 396]]
m.LikeThisTask.itemId = m.top.parentId
m.LikeThisTask.control = "RUN"
end sub
@ -86,6 +107,7 @@ sub onLikeThisLoaded()
end for
addRowSize([234, 396])
end if
' Special Features next...
m.SpecialFeaturesTask.itemId = m.top.parentId
m.SpecialFeaturesTask.control = "RUN"
end sub

View File

@ -5,7 +5,7 @@
<field id="type" type="string" />
<field id="parentId" type="string" />
<field id="selectedItem" type="node" alwaysNotify="true" />
<function name="loadPeople" />
<function name="loadParts" />
<function name="loadPersonVideos" />
</interface>
<script type="text/brightscript" uri="ExtrasRowList.brs" />

View File

@ -16,4 +16,5 @@
<script type="text/brightscript" uri="Home.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -2,9 +2,16 @@ sub init()
m.itemText = m.top.findNode("itemText")
m.itemPoster = m.top.findNode("itemPoster")
m.itemProgress = m.top.findNode("progress")
m.itemProgressBackground = m.top.findNode("progressBackground")
m.itemIcon = m.top.findNode("itemIcon")
m.itemTextExtra = m.top.findNode("itemTextExtra")
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.showProgressBarAnimation = m.top.findNode("showProgressBar")
m.showProgressBarField = m.top.findNode("showProgressBarField")
' Randomize the background colors
m.backdrop = m.top.findNode("backdrop")
@ -19,19 +26,26 @@ sub itemContentChanged()
if itemData = invalid then return
itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem"
m.itemPoster.width = itemData.imageWidth
m.itemText.maxWidth = itemData.imageWidth
m.itemTextExtra.width = itemData.imageWidth
m.itemTextExtra.visible = true
m.backdrop.width = itemData.imageWidth
if itemData.iconUrl <> invalid
m.itemIcon.uri = itemData.iconUrl
end if
if LCase(itemData.type) = "series"
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
end if
end if
end if
' Format the Data based on the type of Home Data
if itemData.type = "CollectionFolder" or itemData.type = "UserView" or itemData.type = "Channel"
m.itemText.text = itemData.name
@ -59,8 +73,12 @@ sub itemContentChanged()
' "Program" is from clicking on an "On Now" item on the Home Screen
if itemData.type = "Program"
m.itemText.Text = itemData.json.name
if itemData.json.ImageURL <> invalid
m.itemPoster.uri = itemData.json.ImageURL
m.itemTextExtra.Text = itemData.json.ChannelName
if itemData.widePosterURL <> ""
m.itemPoster.uri = ImageURL(itemData.widePosterURL)
else
m.itemPoster.uri = ImageURL(itemData.json.ChannelId)
m.itemPoster.loadDisplayMode = "scaleToFill"
end if
' Set Episode title if available
@ -74,6 +92,10 @@ sub itemContentChanged()
if itemData.type = "Episode"
m.itemText.text = itemData.json.SeriesName
if itemData.PlayedPercentage > 0
drawProgressBar(itemData)
end if
if itemData.usePoster = true
m.itemPoster.uri = itemData.widePosterURL
else
@ -99,6 +121,10 @@ sub itemContentChanged()
if itemData.type = "Movie"
m.itemText.text = itemData.name
if itemData.PlayedPercentage > 0
drawProgressBar(itemData)
end if
' Use best image, but fallback to secondary if it's empty
if (itemData.imageWidth = 180 and itemData.posterURL <> "") or itemData.thumbnailURL = ""
m.itemPoster.uri = itemData.posterURL
@ -126,6 +152,10 @@ sub itemContentChanged()
if itemData.type = "Video"
m.itemText.text = itemData.name
if itemData.PlayedPercentage > 0
drawProgressBar(itemData)
end if
if itemData.imageWidth = 180
m.itemPoster.uri = itemData.posterURL
else
@ -133,6 +163,7 @@ sub itemContentChanged()
end if
return
end if
if itemData.type = "Series"
m.itemText.text = itemData.name
@ -170,10 +201,47 @@ sub itemContentChanged()
return
end if
if itemData.type = "MusicArtist"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "Audio"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "TvChannel"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "Season"
m.itemText.text = itemData.json.SeriesName
m.itemTextExtra.text = itemData.name
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
print "Unhandled Home Item Type: " + itemData.type
end sub
'
' Draws and animates item progress bar
sub drawProgressBar(itemData)
m.itemProgressBackground.width = itemData.imageWidth
m.itemProgressBackground.visible = true
m.showProgressBarField.keyValue = [0, m.itemPoster.width * (itemData.PlayedPercentage / 100)]
m.showProgressBarAnimation.control = "Start"
end sub
'
' Enable title scrolling based on item Focus
sub focusChanged()

View File

@ -3,9 +3,20 @@
<children>
<Rectangle id="backdrop" width="464" height="261" translation="[8,5]" />
<Poster id="itemIcon" width="100" height="100" translation="[190,85]" loadDisplayMode="scaleToFit" />
<Poster id="itemPoster" width="464" height="261" translation="[8,5]" loadDisplayMode="scaleToZoom" />
<Poster id="itemPoster" width="464" height="261" translation="[8,5]" loadDisplayMode="scaleToZoom">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[375, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<Rectangle id="progressBackground" visible="false" color="0x00000098" width="464" height="8" translation="[8,260]">
<Rectangle id="progress" color="#00a4dcFF" width="0" height="8" />
</Rectangle>
<ScrollingLabel id="itemText" horizAlign="center" vertAlign="center" font="font:SmallBoldSystemFont" height="64" maxWidth="456" translation="[8,267]" repeatCount="0" />
<Label id="itemTextExtra" horizAlign="left" vertAlign="center" font="font:SmallBoldSystemFont" height="32" width="456" translation="[8,300]" visible="false" color="#777777FF" />
<Animation id="showProgressBar" delay="1" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id="showProgressBarField" key="[0.0, 1.0]" fieldToInterp="progress.width" />
</Animation>
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />

View File

@ -19,13 +19,19 @@ sub init()
' Load the Libraries from API via task
m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask")
m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
' set up tesk nodes for other rows
m.LoadContinueTask = createObject("roSGNode", "LoadItemsTask")
m.LoadContinueTask.itemsToLoad = "continue"
m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
m.LoadNextUpTask.itemsToLoad = "nextUp"
m.LoadOnNowTask = createObject("roSGNode", "LoadItemsTask")
m.LoadOnNowTask.itemsToLoad = "onNow"
m.LoadFavoritesTask = createObject("roSGNode", "LoadItemsTask")
m.LoadFavoritesTask.itemsToLoad = "favorites"
end sub
sub loadLibraries()
@ -55,55 +61,85 @@ sub onLibrariesLoaded()
m.LoadLibrariesTask.content = []
' create My Media, Continue Watching, and Next Up rows
content = CreateObject("roSGNode", "ContentNode")
mediaRow = content.CreateChild("HomeRow")
mediaRow.title = tr("My Media")
continueRow = content.CreateChild("HomeRow")
continueRow.title = tr("Continue Watching")
nextUpRow = content.CreateChild("HomeRow")
nextUpRow.title = tr("Next Up >")
favoritesRow = content.CreateChild("HomeRow")
favoritesRow.title = tr("Favorites")
sizeArray = [
[464, 311], ' My Media
[464, 331], ' Continue Watching
[464, 331] ' Next Up
[464, 331], ' Next Up
[464, 331] ' Favorites
]
haveLiveTV = false
' Load the NextUp Data
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
' Load the Continue Watching Data
m.LoadContinueTask.observeField("content", "updateContinueItems")
m.LoadContinueTask.control = "RUN"
' Load the Favorites Data
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
m.LoadFavoritesTask.control = "RUN"
' validate library data
if m.libraryData <> invalid and m.libraryData.count() > 0
userConfig = m.top.userConfig
' populate My Media row
filteredMedia = filterNodeArray(m.libraryData, "id", userConfig.MyMediaExcludes)
for each item in filteredMedia
mediaRow.appendChild(item)
end for
' create a "Latest In" row for each library
filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes)
for each lib in filteredLatest
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv"
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])
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
metadata = { "title": lib.name }
metadata.Append({ "contentType": lib.json.CollectionType })
loadLatest.metadata = metadata
loadLatest.observeField("content", "updateLatestItems")
loadLatest.control = "RUN"
else if lib.collectionType = "livetv"
' If we have Live TV, add "On Now"
onNowRow = content.CreateChild("HomeRow")
onNowRow.title = tr("On Now")
sizeArray.Push([464, 331])
haveLiveTV = true
' If we have Live TV access, load "On Now" data
if haveLiveTV
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end if
end if
end for
end if
m.top.rowItemSize = sizeArray
m.top.content = content
' Load the Continue Watching Data
m.LoadContinueTask.observeField("content", "updateContinueItems")
m.LoadContinueTask.control = "RUN"
' If we have Live TV access, load "On Now" data
if haveLiveTV
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end if
end sub
sub updateHomeRows()
@ -116,6 +152,51 @@ sub updateHomeRows()
m.LoadContinueTask.control = "RUN"
end sub
sub updateFavoritesItems()
itemData = m.LoadFavoritesTask.content
m.LoadFavoritesTask.unobserveField("content")
m.LoadFavoritesTask.content = []
if itemData = invalid then return
homeRows = m.top.content
rowIndex = getRowIndex("Favorites")
if itemData.count() < 1
if rowIndex <> invalid
' remove the row
deleteFromSizeArray(rowIndex)
homeRows.removeChildIndex(rowIndex)
end if
else
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Favorites")
itemSize = [464, 331]
for each item in itemData
usePoster = true
if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
usePoster = false
end if
item.usePoster = usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
if rowIndex = invalid
' insert new row under "My Media"
updateSizeArray(itemSize, 1)
homeRows.insertChild(row, 1)
else
' replace the old row
homeRows.replaceChild(row, rowIndex)
end if
end if
end sub
sub updateContinueItems()
itemData = m.LoadContinueTask.content
m.LoadContinueTask.unobserveField("content")
@ -138,6 +219,10 @@ sub updateContinueItems()
row.title = tr("Continue Watching")
itemSize = [464, 331]
for each item in itemData
if item.json?.UserData?.PlayedPercentage <> invalid
item.PlayedPercentage = item.json.UserData.PlayedPercentage
end if
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
@ -152,9 +237,6 @@ sub updateContinueItems()
homeRows.replaceChild(row, continueRowIndex)
end if
end if
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
end sub
sub updateNextUpItems()
@ -207,24 +289,6 @@ sub updateNextUpItems()
m.global.app_loaded = true
end if
' create task nodes for "Latest In" rows
userConfig = m.top.userConfig
filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes)
for each lib in filteredLatest
if lib.collectionType <> "livetv" and lib.collectionType <> "boxsets" and lib.json.CollectionType <> "Program"
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
metadata = { "title": lib.name }
metadata.Append({ "contentType": lib.json.CollectionType })
loadLatest.metadata = metadata
loadLatest.observeField("content", "updateLatestItems")
loadLatest.control = "RUN"
end if
end for
end sub
sub updateLatestItems(msg)

View File

@ -30,6 +30,7 @@ sub loadItems()
params["ParentId"] = m.top.itemId
params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
params["ImageTypeLimit"] = 1
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
@ -53,6 +54,10 @@ sub loadItems()
params["SortOrder"] = "Descending"
params["ImageTypeLimit"] = 1
params["UserId"] = get_setting("active_user")
params["EnableRewatching"] = false
params["DisableFirstEpisode"] = false
params["limit"] = 24
params["EnableTotalRecordCount"] = false
maxDaysInNextUp = get_user_setting("ui.details.maxdaysnextup", "365")
if isValid(maxDaysInNextUp)
@ -64,9 +69,6 @@ sub loadItems()
dateCutoff.FromSeconds(dateToday.AsSeconds() - (maxDaysInNextUp * 86400))
params["NextUpDateCutoff"] = dateCutoff.ToISOString()
params["EnableRewatching"] = false
params["DisableFirstEpisode"] = false
params["limit"] = 24
end if
end if
@ -88,6 +90,29 @@ sub loadItems()
params["SortBy"] = "DatePlayed"
params["SortOrder"] = "Descending"
params["Filters"] = "IsResumable"
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
' Skip Books for now as we don't support it (issue #558)
if item.Type <> "Book"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
else if m.top.itemsToLoad = "favorites"
url = Substitute("Users/{0}/Items", get_setting("active_user"))
params = {}
params["Filters"] = "IsFavorite"
params["Limit"] = 20
params["recursive"] = true
params["sortby"] = "random"
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
@ -151,6 +176,20 @@ sub loadItems()
tmp.json = specfeat
end for
end if
else if m.top.itemsToLoad = "additionalparts"
additionalParts = api_API().videos.getAdditionalParts(m.top.itemId)
if isValid(additionalParts)
for each part in additionalParts.items
tmp = CreateObject("roSGNode", "ExtrasData")
params = {}
params["Tags"] = part.ImageTags.Primary
params["MaxWidth"] = 450
params["MaxHeight"] = 402
tmp.posterURL = ImageUrl(part.Id, "Primary", params)
tmp.json = part
results.push(tmp)
end for
end if
else if m.top.itemsToLoad = "likethis"
params = { "userId": get_setting("active_user"), "limit": 16 }
url = Substitute("Items/{0}/Similar", m.top.itemId)

View File

@ -15,4 +15,5 @@
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -243,8 +243,8 @@ function getRelativeDayName(date) as string
end if
' Check for Yesterday
todayMidnight = now.AsSeconds() - (now.AsSeconds() MOD 86400)
dateMidnight = date.AsSeconds() - (date.AsSeconds() MOD 86400)
todayMidnight = now.AsSeconds() - (now.AsSeconds() mod 86400)
dateMidnight = date.AsSeconds() - (date.AsSeconds() mod 86400)
if todayMidnight - dateMidnight = 86400
return "yesterday"
@ -266,8 +266,8 @@ function getDurationStringFromSeconds(seconds) as string
minutes = seconds / 60.0
if minutes > 60
hours = (minutes - (minutes MOD 60)) / 60
minutes = minutes MOD 60
hours = (minutes - (minutes mod 60)) / 60
minutes = minutes mod 60
end if
if hours > 0

View File

@ -44,7 +44,14 @@ end sub
'Voice Search set
sub channelsearchTermSet()
m.scheduleGrid.jumpToChannel = 0
if m.top.searchTerm <> invalid and m.LoadChannelsTask.searchTerm <> m.top.searchTerm
'Reset filter if user says all
if LCase(m.top.searchTerm) = LCase(tr("all")) or m.LoadChannelsTask.searchTerm = LCase(tr("all"))
m.top.searchTerm = " "
m.LoadChannelsTask.searchTerm = " "
m.spinner.visible = true
m.LoadChannelsTask.control = "RUN"
'filter if the searterm is not invalid
else if m.top.searchTerm <> invalid and LCase(m.LoadChannelsTask.searchTerm) <> LCase(m.top.searchTerm)
if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop"
m.LoadChannelsTask.searchTerm = m.top.searchTerm
@ -279,6 +286,7 @@ end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
detailsGrp = m.top.findNode("detailsPane")
gridGrp = m.top.findNode("scheduleGrid")
@ -288,6 +296,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
gridGrp.setFocus(true)
return true
else if key = "back"
m.LoadChannelsTask.control = "stop"
m.global.sceneManager.callFunc("popScene")
return true
end if

View File

@ -0,0 +1,129 @@
sub init()
m.queue = []
m.position = 0
end sub
'
' Clear all content from play queue
sub clear()
m.queue = []
setPosition(0)
end sub
'
' Delete item from play queue at passed index
sub deleteAtIndex(index)
m.queue.Delete(index)
end sub
'
' Return the number of items in the play queue
function getCount()
return m.queue.count()
end function
'
' Return the item currently in focus from the play queue
function getCurrentItem()
return getItemByIndex(m.position)
end function
'
' Return the item in the passed index from the play queue
function getItemByIndex(index)
return m.queue[index]
end function
'
' Returns current playback position within the queue
function getPosition()
return m.position
end function
'
' Move queue position back one
sub moveBack()
m.position--
end sub
'
' Move queue position ahead one
sub moveForward()
m.position++
end sub
'
' Return the current play queue
function getQueue()
return m.queue
end function
'
' Return item at end of play queue without removing
function peek()
return m.queue.peek()
end function
'
' Play items in queue
sub playQueue()
nextItem = top()
nextItemMediaType = invalid
if isValid(nextItem?.json?.mediatype) and nextItem.json.mediatype <> ""
nextItemMediaType = LCase(nextItem.json.mediatype)
else if isValid(nextItem?.type) and nextItem.type <> ""
nextItemMediaType = LCase(nextItem.type)
end if
if not isValid(nextItemMediaType) then return
if nextItemMediaType = "audio"
CreateAudioPlayerView()
end if
end sub
'
' Remove item at end of play queue
sub pop()
m.queue.pop()
end sub
'
' Push new items to the play queue
sub push(newItem)
m.queue.push(newItem)
end sub
'
' Set the queue position
sub setPosition(newPosition)
m.position = newPosition
end sub
'
' Return the fitst item in the play queue
function top()
return getItemByIndex(0)
end function
'
' Replace play queue with passed array
sub set(items)
setPosition(0)
m.queue = items
end sub

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="QueueManager" extends="Group">
<interface>
<function name="clear" />
<function name="deleteAtIndex" />
<function name="getCount" />
<function name="getCurrentItem" />
<function name="getItemByIndex" />
<function name="getPosition" />
<function name="getQueue" />
<function name="moveBack" />
<function name="moveForward" />
<function name="peek" />
<function name="playQueue" />
<function name="pop" />
<function name="push" />
<function name="set" />
<function name="setPosition" />
<function name="top" />
</interface>
<script type="text/brightscript" uri="QueueManager.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="ViewCreator.brs" />
</component>

View File

@ -0,0 +1,6 @@
' Play Audio
sub CreateAudioPlayerView()
view = CreateObject("roSGNode", "AudioPlayerView")
view.observeField("state", m.port)
m.global.sceneManager.callFunc("pushScene", view)
end sub

View File

@ -58,12 +58,20 @@ sub itemContentChanged()
' Handle all "As Is" fields
m.top.overhangTitle = itemData.name
setFieldText("releaseYear", itemData.productionYear)
setFieldText("officialRating", itemData.officialRating)
setFieldText("overview", itemData.overview)
if itemData.officialRating <> invalid
setFieldText("officialRating", itemData.officialRating)
else
m.top.findNode("infoGroup").removeChild(m.top.findNode("officialRating"))
end if
if itemData.communityRating <> invalid
setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
m.top.findNode("communityRatingGroup").visible = true
m.top.findNode("star").visible = "true"
else
' hide the star icon
m.top.findNode("infoGroup").removeChild(m.top.findNode("communityRatingGroup"))
end if
if itemData.CriticRating <> invalid
@ -87,6 +95,8 @@ sub itemContentChanged()
if itemData.genres.count() > 0
setFieldText("genres", tr("Genres") + ": " + itemData.genres.join(", "))
else
m.top.findNode("details").removeChild(m.top.findNode("genres"))
end if
' show tags if there are no genres to display
@ -102,10 +112,8 @@ sub itemContentChanged()
end for
if directors.count() > 0
setFieldText("director", tr("Director") + ": " + directors.join(", "))
end if
if itemData.mediaStreams[0] <> invalid
setFieldText("video_codec", tr("Video") + ": " + itemData.mediaStreams[0].displayTitle)
else
m.top.findNode("details").removeChild(m.top.findNode("director"))
end if
if get_user_setting("ui.details.hidetagline") = "false"
@ -116,6 +124,15 @@ sub itemContentChanged()
m.details.removeChild(m.tagline)
end if
'set aired date if type is Episode
if itemData.PremiereDate <> invalid and itemData.Type = "Episode"
airDate = CreateObject("roDateTime")
airDate.FromISO8601String(itemData.PremiereDate)
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
'remove movie release year label
m.top.findNode("infoGroup").removeChild(m.top.findNode("releaseYear"))
end if
setFavoriteColor()
setWatchedColor()
SetUpVideoOptions(itemData.mediaSources)
@ -128,11 +145,28 @@ end sub
sub SetUpVideoOptions(streams)
videos = []
codecDetailsSet = false
for i = 0 to streams.Count() - 1
if streams[i].VideoType = "VideoFile"
codec = ""
if streams[i].mediaStreams <> invalid and streams[i].mediaStreams.Count() > 0 then codec = streams[i].mediaStreams[0].displayTitle
if streams[i].mediaStreams <> invalid and streams[i].mediaStreams.Count() > 0
' find the first (default) video track to get the codec for the details screen
if codecDetailsSet = false
for index = 0 to streams[i].mediaStreams.Count() - 1
if streams[i].mediaStreams[index].Type = "Video"
setFieldText("video_codec", tr("Video") + ": " + streams[i].mediaStreams[index].displayTitle)
codecDetailsSet = true
exit for
end if
end for
end if
codec = streams[i].mediaStreams[0].displayTitle
end if
' Create options for user to switch between video tracks
videos.push({
"Title": streams[i].Name,
"Description": tr("Video"),

View File

@ -9,7 +9,7 @@
<Label id="runtime" />
<Label id="officialRating" />
<LayoutGroup id="communityRatingGroup" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" />
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" visible="false"/>
<Label id="communityRating" />
</LayoutGroup>
<LayoutGroup layoutDirection="horiz" itemSpacings="[-5]" id="criticRatingGroup">
@ -17,6 +17,7 @@
<Label id="criticRatingLabel" />
</LayoutGroup>
<Label id="ends-at" />
<Label id="aired" />
</LayoutGroup>
<Label id="genres" />
<Label id="director" />

View File

@ -8,11 +8,8 @@ sub init()
setupDataTasks()
setupScreenSaver()
m.currentSongIndex = 0
m.buttonsNeedToBeLoaded = true
m.shuffleEnabled = false
m.loopMode = ""
m.shuffleEvent = ""
m.buttonCount = m.buttons.getChildCount()
m.playReported = false
@ -26,6 +23,9 @@ sub init()
' Write screen tracker for screensaver
WriteAsciiFile("tmp:/scene.temp", "nowplaying")
MoveFile("tmp:/scene.temp", "tmp:/scene")
loadButtons()
pageContentChanged()
end sub
sub onScreensaverTimeoutLoaded()
@ -62,7 +62,7 @@ sub setupAnimationTasks()
m.screenSaverStartAnimation = m.top.FindNode("screenSaverStartAnimation")
end sub
' Creates tasks to gather data needed to renger NowPlaying Scene and play song
' Creates tasks to gather data needed to render Scene and play song
sub setupDataTasks()
' Load meta data
m.LoadMetaDataTask = CreateObject("roSGNode", "LoadItemsTask")
@ -219,18 +219,25 @@ sub audioStateChanged()
' Song Finished, attempt to move to next song
if m.top.audio.state = "finished"
' User has enabled single song loop, play current song again
if m.loopMode = "one"
playAction()
return
else if m.loopMode = "all"
m.currentSongIndex = -1
LoadNextSong()
return
end if
if m.currentSongIndex < m.top.pageContent.count() - 1
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
' We are not at the end of the song queue, advance to next song
LoadNextSong()
else
' We are at the end of the song queue
' User has enabled loop for entire song queue, move back to first song
if m.loopMode = "all"
m.global.queueManager.callFunc("setPosition", -1)
LoadNextSong()
return
end if
' Return to previous screen
m.top.state = "finished"
end if
@ -263,8 +270,8 @@ function previousClicked() as boolean
m.top.audio.control = "stop"
end if
if m.currentSongIndex > 0
m.currentSongIndex--
if m.global.queueManager.callFunc("getPosition") > 0
m.global.queueManager.callFunc("moveBack")
pageContentChanged()
end if
@ -289,7 +296,7 @@ function loopClicked() as boolean
end function
function nextClicked() as boolean
if m.currentSongIndex < m.top.pageContent.count() - 1
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
LoadNextSong()
end if
@ -302,7 +309,7 @@ end sub
function findCurrentSongIndex(songList) as integer
for i = 0 to songList.count() - 1
if songList[i] = m.top.pageContent[m.currentSongIndex]
if songList[i].id = m.global.queueManager.callFunc("getCurrentItem").id
return i
end if
end for
@ -318,10 +325,10 @@ function shuffleClicked() as boolean
m.shuffleIndicator.opacity = ".4"
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-on", "-off")
m.shuffleEvent = "enabled"
m.currentSongIndex = findCurrentSongIndex(m.originalSongList)
m.top.pageContent = m.originalSongList
setFieldTextValue("numberofsongs", "Track " + stri(m.currentSongIndex + 1) + "/" + stri(m.top.pageContent.count()))
currentSongIndex = findCurrentSongIndex(m.originalSongList)
m.global.queueManager.callFunc("set", m.originalSongList)
m.global.queueManager.callFunc("setPosition", currentSongIndex)
setFieldTextValue("numberofsongs", "Track " + stri(m.global.queueManager.callFunc("getPosition") + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
return true
end if
@ -329,14 +336,14 @@ function shuffleClicked() as boolean
m.shuffleIndicator.opacity = "1"
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
m.originalSongList = m.top.pageContent
m.originalSongList = m.global.queueManager.callFunc("getQueue")
songIDArray = m.top.pageContent
songIDArray = m.global.queueManager.callFunc("getQueue")
' Move the currently playing song to the front of the queue
temp = m.top.pageContent[0]
songIDArray[0] = m.top.pageContent[m.currentSongIndex]
songIDArray[m.currentSongIndex] = temp
temp = m.global.queueManager.callFunc("top")
songIDArray[0] = m.global.queueManager.callFunc("getCurrentItem")
songIDArray[m.global.queueManager.callFunc("getPosition")] = temp
for i = 1 to songIDArray.count() - 1
j = Rnd(songIDArray.count() - 1)
@ -345,10 +352,7 @@ function shuffleClicked() as boolean
songIDArray[j] = temp
end for
m.currentSongIndex = 0
m.shuffleEvent = "enabled"
m.top.pageContent = songIDArray
m.global.queueManager.callFunc("set", songIDArray)
return true
end function
@ -360,30 +364,64 @@ sub LoadNextSong()
' Reset playPosition bar without animation
m.playPosition.width = 0
m.currentSongIndex++
m.global.queueManager.callFunc("moveForward")
pageContentChanged()
end sub
' Update values on screen when page content changes
sub pageContentChanged()
' pageContent Changed due to shuffle event, don't update screen values
if m.shuffleEvent = "enabled"
m.shuffleEvent = ""
return
end if
' Reset buffer bar without animation
m.bufferPosition.width = 0
m.LoadMetaDataTask.itemId = m.top.pageContent[m.currentSongIndex]
m.LoadMetaDataTask.observeField("content", "onMetaDataLoaded")
m.LoadMetaDataTask.control = "RUN"
useMetaTask = false
currentItem = m.global.queueManager.callFunc("getCurrentItem")
m.LoadAudioStreamTask.itemId = m.top.pageContent[m.currentSongIndex]
if not isValid(currentItem.RunTimeTicks)
useMetaTask = true
end if
if not isValid(currentItem.AlbumArtist)
useMetaTask = true
end if
if not isValid(currentItem.name)
useMetaTask = true
end if
if not isValid(currentItem.Artists)
useMetaTask = true
end if
if useMetaTask
m.LoadMetaDataTask.itemId = currentItem.id
m.LoadMetaDataTask.observeField("content", "onMetaDataLoaded")
m.LoadMetaDataTask.control = "RUN"
else
if isValid(currentItem.ParentBackdropItemId)
setBackdropImage(ImageURL(currentItem.ParentBackdropItemId, "Backdrop", { "maxHeight": "720", "maxWidth": "1280" }))
end if
setPosterImage(ImageURL(currentItem.id, "Primary", { "maxHeight": 500, "maxWidth": 500 }))
setScreenTitle(currentItem)
setOnScreenTextValues(currentItem)
m.songDuration = currentItem.RunTimeTicks / 10000000.0
end if
m.LoadAudioStreamTask.itemId = currentItem.id
m.LoadAudioStreamTask.observeField("content", "onAudioStreamLoaded")
m.LoadAudioStreamTask.control = "RUN"
end sub
' If we have more and 1 song to play, fade in the next and previous controls
sub loadButtons()
if m.global.queueManager.callFunc("getCount") > 1
m.shuffleIndicator.opacity = ".4"
m.loopIndicator.opacity = ".4"
m.displayButtonsAnimation.control = "start"
end if
end sub
sub onAudioStreamLoaded()
data = m.LoadAudioStreamTask.content[0]
m.LoadAudioStreamTask.unobserveField("content")
@ -408,7 +446,7 @@ sub onMetaDataLoaded()
if data <> invalid and data.count() > 0
' Use metadata to load backdrop image
if isvalid(data.json)
if isValid(data.json)
if isValid(data.json.ArtistItems)
if data.json.ArtistItems.count() > 0
if isValid(data.json.ArtistItems[0].id)
@ -425,16 +463,6 @@ sub onMetaDataLoaded()
setOnScreenTextValues(data.json)
m.songDuration = data.json.RunTimeTicks / 10000000.0
' If we have more and 1 song to play, fade in the next and previous controls
if m.buttonsNeedToBeLoaded
if m.top.pageContent.count() > 1
m.shuffleIndicator.opacity = ".4"
m.loopIndicator.opacity = ".4"
m.displayButtonsAnimation.control = "start"
end if
m.buttonsNeedToBeLoaded = false
end if
end if
end sub
@ -471,12 +499,12 @@ end sub
' Populate on screen text variables
sub setOnScreenTextValues(json)
if isValid(json)
currentSongIndex = m.currentSongIndex
currentSongIndex = m.global.queueManager.callFunc("getPosition")
if m.shuffleEnabled
currentSongIndex = findCurrentSongIndex(m.originalSongList)
end if
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.top.pageContent.count()))
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
setFieldTextValue("artist", json.Artists[0])
setFieldTextValue("song", json.name)
end if
@ -511,7 +539,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
else if key = "fastforward"
return nextClicked()
else if key = "left"
if m.top.pageContent.count() = 1 then return false
if m.global.queueManager.callFunc("getCount") = 1 then return false
if m.top.selectedButtonIndex > 0
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
@ -519,7 +547,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
return true
else if key = "right"
if m.top.pageContent.count() = 1 then return false
if m.global.queueManager.callFunc("getCount") = 1 then return false
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
@ -554,7 +582,7 @@ sub ReportPlayback(state = "update" as string)
if m.top.audio.position = invalid then return
params = {
"ItemId": m.top.pageContent[m.currentSongIndex],
"ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
"PlaySessionId": m.top.audio.content.id,
"PositionTicks": int(m.top.audio.position) * 10000000&, 'Ensure a LongInteger is used
"IsPaused": (m.top.audio.state = "paused")

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="NowPlaying" extends="JFScreen">
<component name="AudioPlayerView" extends="JFScreen">
<children>
<Poster id="backdrop" opacity=".5" loadDisplayMode="scaleToZoom" width="1920" height="1200" blendColor="#3f3f3f" />
<Poster id="shuffleIndicator" width="64" height="64" uri="pkg:/images/icons/shuffleIndicator-off.png" translation="[1150,775]" opacity="0" />
@ -8,7 +8,7 @@
<LayoutGroup id="main_group" layoutDirection="vert" horizAlignment="center" itemSpacings="[15]">
<Poster id="albumCover" width="500" height="500" />
<Label id="artist" width="900" height="25" horizAlign="center" />
<Label id="song" width="900" height="25" horizAlign="center" />
<Label id="song" width="900" height="25" horizAlign="center" />
<Label id="numberofsongs" width="500" height="25" horizAlign="center" font="font:SmallestSystemFont" color="#999999" />
</LayoutGroup>
<Rectangle id="seekBar" color="0x00000099" width="500" height="10">
@ -77,7 +77,7 @@
<Vector2DFieldInterpolator id="OneInterp" key="[0.0,1.0]" keyValue="[[450,30],[960,575]]" fieldToInterp="screenSaverAlbumCover.translation" />
</Animation>
</SequentialAnimation>
<!-- Jellyfin Logo ScreenSaver -->
<SequentialAnimation id="BounceAnimation" repeat="true">
<Animation id="AnimOne" repeat="false" easeFunction="linear" duration="7.2">
@ -119,13 +119,12 @@
<Poster width="0" height="0" uri="pkg:/images/icons/loopIndicator1-on.png" visible="false" />
</children>
<interface>
<field id="pageContent" type="array" onChange="pageContentChanged" />
<field id="audio" type="node" />
<field id="state" type="string" />
<field id="selectedButtonIndex" type="integer" />
</interface>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="NowPlaying.brs" />
<script type="text/brightscript" uri="AudioPlayerView.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />

View File

@ -1,21 +1,123 @@
sub init()
m.top.optionsAvailable = false ' Change once Shuffle option is added
m.top.optionsAvailable = true
m.top.overhangVisible = false
m.slideshowTimer = m.top.findNode("slideshowTimer")
m.slideshowTimer.observeField("fire", "nextSlide")
m.status = m.top.findNode("status")
m.textBackground = m.top.findNode("background")
m.statusTimer = m.top.findNode("statusTimer")
m.statusTimer.observeField("fire", "statusUpdate")
m.slideshow = get_user_setting("photos.slideshow")
m.random = get_user_setting("photos.random")
m.showStatusAnimation = m.top.findNode("showStatusAnimation")
m.hideStatusAnimation = m.top.findNode("hideStatusAnimation")
itemContentChanged()
end sub
sub itemContentChanged()
m.LoadLibrariesTask = createObject("roSGNode", "LoadPhotoTask")
m.LoadLibrariesTask.itemContent = m.top.itemContent
m.LoadLibrariesTask.observeField("results", "onPhotoLoaded")
m.LoadLibrariesTask.control = "RUN"
if isValidToContinue(m.top.itemIndex)
m.LoadLibrariesTask = createObject("roSGNode", "LoadPhotoTask")
itemContent = m.top.items.content.getChild(m.top.itemIndex)
m.LoadLibrariesTask.itemContent = itemContent
m.LoadLibrariesTask.observeField("results", "onPhotoLoaded")
m.LoadLibrariesTask.control = "RUN"
end if
end sub
sub onPhotoLoaded()
if m.LoadLibrariesTask.results <> invalid
photo = m.top.findNode("photo")
photo.uri = m.LoadLibrariesTask.results
if m.slideshow = "true" or m.random = "true"
' user has requested either a slideshow or random...
m.slideshowTimer.control = "start"
end if
else
'Show user error here (for example if it's not a supported image type)
message_dialog("This image type is not supported.")
end if
end sub
sub nextSlide()
m.slideshowTimer.control = "stop"
if m.slideshow = "true"
if isValidToContinue(m.top.itemIndex + 1)
m.top.itemIndex++
m.slideshowTimer.control = "start"
end if
else if m.random = "true"
index = rnd(m.top.items.content.getChildCount() - 1)
if isValidToContinue(index)
m.top.itemIndex = index
m.slideshowTimer.control = "start"
end if
end if
end sub
sub statusUpdate()
m.statusTimer.control = "stop"
m.hideStatusAnimation.control = "start"
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "right"
if isValidToContinue(m.top.itemIndex + 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex++
end if
return true
end if
if key = "left"
if isValidToContinue(m.top.itemIndex - 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex--
end if
return true
end if
if key = "play"
if m.slideshowTimer.control = "start"
' stop the slideshow if the user hits "pause"
m.slideshowTimer.control = "stop"
m.status.text = tr("Slideshow Paused")
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.statusTimer.control = "start"
else
' start the slideshow if the user hits "play"
m.status.text = tr("Slideshow Resumed")
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.slideshow = "true"
m.statusTimer.control = "start"
m.slideshowTimer.control = "start"
end if
return true
end if
if key = "options"
' Options (random etc) is done on itemGrid
return true
end if
return false
end function
function isValidToContinue(index as integer)
if isValid(m.top.items) and isValid(m.top.items.content)
if index >= 0 and index < m.top.items.content.getChildCount()
return true
end if
end if
return false
end function

View File

@ -1,13 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="PhotoDetails" extends="JFGroup">
<children>
<LayoutGroup id="toplevel">
<Poster id="photo" width="1920" height="1080" loadDisplayMode="scaleToFit"/>
</LayoutGroup>
<Poster id="photo" width="1920" height="1080" loadDisplayMode="scaleToFit"/>
<Rectangle id="background" color="0x101010EE" height="120" width="500" Translation="[700, -150]" opacity="0">
<Label id="status" font="font:MediumSystemFont" height="100" width="500" horizAlign="center" vertAlign="bottom"/>
</Rectangle>
<Timer id="slideshowTimer" duration="5" repeat="false" />
<Timer id="statusTimer" duration="2" repeat="false" />
<Animation id="showStatusAnimation" duration="1" repeat="false">
<FloatFieldInterpolator key="[0.0, 0.1]" keyValue="[0, 1]" fieldToInterp="background.opacity" />
<Vector2DFieldInterpolator key="[0.1, 1]" keyValue="[[700, -150], [700, -5]]" fieldToInterp="background.translation" />
</Animation>
<Animation id="hideStatusAnimation" duration="1" repeat="false">
<Vector2DFieldInterpolator key="[0.0, 0.9]" keyValue="[[700, -5], [700, -150]]" fieldToInterp="background.translation" />
<FloatFieldInterpolator key="[0.9, 1]" keyValue="[1, 0]" fieldToInterp="background.opacity" />
</Animation>
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="items" type="node" />
<field id="itemIndex" type="integer" value="-1" onChange="itemContentChanged" />
</interface>
<script type="text/brightscript" uri="PhotoDetails.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -1,17 +0,0 @@
sub init()
m.top.functionName = "loadItems"
end sub
sub loadItems()
item = m.top.itemContent
group = CreateObject("roSGNode", "PhotoDetails")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = item
' TODO/FIXME:
' Wait some time and move to the next photo...
end sub

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="PhotoPlayerTask" extends="Task">
<interface>
<field id="itemContent" type="node" />
</interface>
<script type="text/brightscript" uri="PhotoPlayerTask.brs" />
</component>

View File

@ -48,7 +48,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.searchAlphabox.textEditBox.translation = "[0, 0]"
end if
if key = "left" and m.searchSelect.isinFocusChain() and (m.searchSelect.currFocusColumn = -1 or m.searchSelect.currFocusColumn = 0)
if key = "left" and m.searchSelect.isinFocusChain()
m.searchAlphabox.setFocus(true)
return true
else if key = "right"

View File

@ -13,6 +13,8 @@ sub init()
m.boolSetting = m.top.findNode("boolSetting")
m.integerSetting = m.top.findNode("integerSetting")
m.radioSetting = m.top.findNode("radioSetting")
m.integerSetting.observeField("submit", "onKeyGridSubmit")
m.integerSetting.observeField("escape", "onKeyGridEscape")
@ -21,6 +23,7 @@ sub init()
m.settingsMenu.observeField("itemSelected", "settingSelected")
m.boolSetting.observeField("checkedItem", "boolSettingChanged")
m.radioSetting.observeField("checkedItem", "radioSettingChanged")
' Load Configuration Tree
m.configTree = GetConfigTree()
@ -40,7 +43,6 @@ sub onKeyGridEscape()
end sub
sub LoadMenu(configSection)
if configSection.children = invalid
' Load parent menu
m.userLocation.pop()
@ -81,6 +83,7 @@ sub settingFocused()
' Hide Settings
m.boolSetting.visible = false
m.integerSetting.visible = false
m.radioSetting.visible = false
if selectedSetting.type = invalid
return
@ -99,6 +102,26 @@ sub settingFocused()
m.integerSetting.text = integerValue
end if
m.integerSetting.visible = true
else if LCase(selectedSetting.type) = "radio"
selectedValue = get_user_setting(selectedSetting.settingName, selectedSetting.default)
radioContent = CreateObject("roSGNode", "ContentNode")
itemIndex = 0
for each item in m.userLocation.peek().children[m.settingsMenu.itemFocused].options
listItem = radioContent.CreateChild("ContentNode")
listItem.title = tr(item.title)
listItem.id = item.id
if selectedValue = item.id
m.radioSetting.checkedItem = itemIndex
end if
itemIndex++
end for
m.radioSetting.content = radioContent
m.radioSetting.visible = true
else
print "Unknown setting type " + selectedSetting.type
end if
@ -117,6 +140,9 @@ sub settingSelected()
if selectedItem.type = "integer"
m.integerSetting.setFocus(true)
end if
if (selectedItem.type) = "radio"
m.radioSetting.setFocus(true)
end if
else if selectedItem.children <> invalid and selectedItem.children.Count() > 0 ' Show sub menu
LoadMenu(selectedItem)
m.settingsMenu.setFocus(true)
@ -139,9 +165,13 @@ sub boolSettingChanged()
else
set_user_setting(selectedSetting.settingName, "false")
end if
end sub
sub radioSettingChanged()
if m.radioSetting.focusedChild = invalid then return
selectedSetting = m.userLocation.peek().children[m.settingsMenu.itemFocused]
set_user_setting(selectedSetting.settingName, m.radioSetting.content.getChild(m.radioSetting.checkedItem).id)
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
@ -152,6 +182,14 @@ function onKeyEvent(key as string, press as boolean) as boolean
else if (key = "back" or key = "left") and m.settingDetail.focusedChild <> invalid
m.settingsMenu.setFocus(true)
return true
else if (key = "back" or key = "left") and m.radioSetting.hasFocus()
m.settingsMenu.setFocus(true)
return true
end if
if key = "options"
m.global.sceneManager.callFunc("popScene")
return true
end if
if key = "right"

View File

@ -31,6 +31,7 @@
</RadioButtonList>
</LayoutGroup>
<RadioButtonList id="radioSetting" translation="[900, 450]" inheritParentTransform="false" vertFocusAnimationStyle="floatingFocus" />
<intkeyboard_integerKeyboard translation="[900, 520]" id="integerSetting" maxLength="3" domain="numeric" visible="false" />
</children>

View File

@ -6,6 +6,8 @@ sub init()
m.Random = m.top.findNode("Random")
m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.rows.observeField("doneLoading", "updateSeason")
end sub
@ -15,6 +17,13 @@ sub setSeasonLoading()
end sub
sub updateSeason()
if m.top.seasonData?.UserData?.UnplayedItemCount <> invalid
if m.top.seasonData.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
end if
end if
imgParams = { "maxHeight": 450, "maxWidth": 300 }
m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
m.Random.visible = true
@ -54,8 +63,10 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
if press and key = "play" or proceed = true
m.top.lastFocus = focusedChild
itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
if itemToPlay <> invalid and itemToPlay.id <> ""
itemToPlay.type = "Episode"
m.top.quickPlayNode = itemToPlay
end if
handled = true

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="TVEpisodes" extends="JFGroup">
<children>
<Poster id="seasonPoster" width="300" height="450" translation="[95,175]" />
<Poster id="seasonPoster" width="300" height="450" translation="[95,175]">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[210, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<JFButton id="Random" text="Play Random" translation="[90, 640]" visible="false"></JFButton>
<TVEpisodeRowWithOptions id="picker" visible="true" />
</children>

View File

@ -5,6 +5,14 @@ sub init()
m.overview = m.top.findNode("overview")
m.poster = m.top.findNode("poster")
m.deviceInfo = CreateObject("roDeviceInfo")
m.rating = m.top.findnode("rating")
m.infoBar = m.top.findnode("infoBar")
m.progressBackground = m.top.findNode("progressBackground")
m.progressBar = m.top.findnode("progressBar")
m.playedIndicator = m.top.findNode("playedIndicator")
m.checkmark = m.top.findNode("checkmark")
m.checkmark.font.size = 35
end sub
sub itemContentChanged()
@ -18,6 +26,12 @@ sub itemContentChanged()
m.title.text = indexNumber + item.title
m.overview.text = item.overview
if itemData.PremiereDate <> invalid
airDate = CreateObject("roDateTime")
airDate.FromISO8601String(itemData.PremiereDate)
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
end if
imageUrl = item.posterURL
if get_user_setting("ui.tvshows.blurunwatched") = "true"
@ -30,18 +44,43 @@ sub itemContentChanged()
m.poster.uri = imageUrl
if type(itemData.RunTimeTicks) = "LongInteger"
m.top.findNode("runtime").text = stri(getRuntime()).trim() + " mins"
if type(itemData.RunTimeTicks) = "roInt" or type(itemData.RunTimeTicks) = "LongInteger"
runTime = getRuntime()
if runTime < 2
m.top.findNode("runtime").text = "1 min"
else
m.top.findNode("runtime").text = stri(runTime).trim() + " mins"
end if
if get_user_setting("ui.design.hideclock") <> "true"
m.top.findNode("endtime").text = tr("Ends at %1").Replace("%1", getEndTime())
end if
end if
if itemData.communityRating <> invalid
m.top.findNode("star").visible = true
m.top.findNode("communityRating").text = str(int(itemData.communityRating * 10) / 10)
if get_user_setting("ui.tvshows.disableCommunityRating") = "false"
if isValid(itemData.communityRating)
m.top.findNode("star").visible = true
m.top.findNode("communityRating").text = str(int(itemData.communityRating * 10) / 10)
else
m.top.findNode("star").visible = false
end if
else
m.top.findNode("star").visible = false
m.rating.visible = false
m.infoBar.itemSpacings = [20, -25, 20, 20]
end if
' Add checkmark in corner (if applicable)
if isValid(itemData?.UserData?.Played) and itemData.UserData.Played = true
m.playedIndicator.visible = true
end if
' Add progress bar on bottom (if applicable)
if isValid(itemData?.UserData?.PlayedPercentage) and itemData?.UserData?.PlayedPercentage > 0
m.progressBackground.width = m.poster.width
m.progressBackground.visible = true
progressWidthInPixels = int(m.progressBackground.width * itemData.UserData.PlayedPercentage / 100)
m.progressBar.width = progressWidthInPixels
m.progressBar.visible = true
end if
videoIdx = invalid

View File

@ -2,19 +2,27 @@
<component name="TVListDetails" extends="Group">
<children>
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[40]">
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[30]">
<Poster id="poster" width="350" height="300" />
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[30]">
<Poster id="poster" width="350" height="300" loadDisplayMode="scaleToZoom">
<Rectangle id="playedIndicator" color="#00a4dcFF" width="60" height="46" visible="false" translation="[290, 0]">
<Label id="checkmark" width="60" height="42" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="bottom" text="✓"/>
</Rectangle>
<Rectangle id="progressBackground" visible="false" color="0x00000098" width="350" height="16" translation="[0,286]">
<Rectangle id="progressBar" color="#00a4dcFF" width="0" height="16" visible="false"/>
</Rectangle>
</Poster>
<LayoutGroup id="text" layoutDirection="vert" itemSpacings="[15]">
<!-- Using poster of 1 length to get spacing. Not successful with adding translation to title -->
<Poster id="null" height="1" />
<ScrollingLabel id="title" font="font:MediumBoldSystemFont" maxWidth="950" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[20]">
<LayoutGroup id="infoBar" layoutDirection="horiz" itemSpacings="[20]">
<Label id="runtime" font="font:SmallestSystemFont" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[-5]">
<LayoutGroup id="rating" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="26" width="26" blendColor="#cb272a" />
<Label id="communityRating" font="font:SmallestSystemFont" />
</LayoutGroup>
<Label id="endtime" font="font:SmallestSystemFont" />
<Label id="aired" font="font:SmallestSystemFont" />
</LayoutGroup>
<Label id="overview" font="font:SmallestSystemFont" wrap="true" height="130" width="950" maxLines="3" ellipsizeOnBoundary="true"/>
<LayoutGroup layoutDirection="horiz" itemSpacings="[15]">

View File

@ -3,6 +3,8 @@ sub init()
main = m.top.findNode("toplevel")
main.translation = [96, 175]
m.extrasSlider = m.top.findNode("tvSeasonExtras")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
'm.extrasSlider.translation = [30,1014]
m.extrasSlider.visible = true
end sub
@ -13,18 +15,42 @@ sub itemContentChanged()
item = m.top.itemContent
itemData = item.json
if itemData?.UserData?.UnplayedItemCount <> invalid
if itemData.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
end if
end if
m.top.findNode("tvshowPoster").uri = m.top.itemContent.posterURL
' Handle all "As Is" fields
m.top.overhangTitle = itemData.name
setFieldText("releaseYear", itemData.productionYear)
setFieldText("officialRating", itemData.officialRating)
'Check production year, if invalid remove label
if itemData.productionYear <> invalid
setFieldText("releaseYear", itemData.productionYear)
else
m.top.findNode("main_group").removeChild(m.top.findNode("releaseYear"))
end if
'Check officialRating, if invalid remove label
if itemData.officialRating <> invalid
setFieldText("officialRating", itemData.officialRating)
else
m.top.findNode("main_group").removeChild(m.top.findNode("officialRating"))
end if
'Check communityRating, if invalid remove label
if itemData.communityRating <> invalid
m.top.findNode("star").visible = true
setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
else
m.top.findNode("main_group").removeChild(m.top.findNode("communityRating"))
m.top.findNode("main_group").removeChild(m.top.findNode("star"))
m.top.findNode("star").visible = false
end if
setFieldText("overview", itemData.overview)
@ -32,11 +58,17 @@ sub itemContentChanged()
setFieldText("runtime", stri(getRuntime()) + " mins")
end if
'History feild is set via the function getHistory()
setFieldText("history", getHistory())
'Check genres, if invalid remove label
if itemData.genres.count() > 0
setFieldText("genres", itemData.genres.join(", "))
else
m.top.findNode("main_group").removeChild(m.top.findNode("genres"))
end if
'We don't display Directors in the show page. Might want to remove this.
for each person in itemData.people
if person.type = "Director"
exit for
@ -44,6 +76,8 @@ sub itemContentChanged()
end for
if itemData.taglines.count() > 0
setFieldText("tagline", itemData.taglines[0])
else
m.top.findNode("main_group").removeChild(m.top.findNode("tagline"))
end if
end sub
@ -105,6 +139,7 @@ function getHistory() as string
end if
if studio = invalid and airwords = invalid
m.top.findNode("main_group").removeChild(m.top.findNode("history"))
return ""
end if

View File

@ -1,15 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="TVShowDetails" extends="JFGroup">
<children>
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[-10]" >
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[15]" >
<Poster id="tvshowPoster" width="300" height="450" />
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[-10]">
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[15]">
<Poster id="tvshowPoster" width="300" height="450">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[210, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
</Poster>
<LayoutGroup layoutDirection="vert" itemSpacings="[15]">
<LayoutGroup layoutDirection="horiz" itemSpacings="[150]">
<Label id="releaseYear" />
<Label id="officialRating" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[3]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" />
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" visible="false" />
<Label id="communityRating" />
</LayoutGroup>
</LayoutGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -3648,5 +3648,745 @@
<translation>Úroveň</translation>
<extracomment>Video profile level</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložit přihlašovací údaje?</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložit přihlašovací údaje?</translation>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>K tomuto interpretovi nebyly nalezeny žádné skladby ani alba</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Pouze textové titulky</translation>
<extracomment>Name of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>Only display text subtitles to minimize transcoding.</source>
<translation>Zobrazit pouze textové titulky pro minimalizaci překódování.</translation>
<extracomment>Description of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>Aired</source>
<translation>Vysíláno</translation>
<extracomment>Aired date label</extracomment>
</message>
<message>
<source>Audio Channels</source>
<translation>Audio kanály</translation>
</message>
<message>
<source>Level</source>
<translation>Úroveň</translation>
<extracomment>Video profile level</extracomment>
</message>
<message>
<source>Bit Rate</source>
<translation>Datový tok</translation>
<extracomment>Video streaming bit rate</extracomment>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Název</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Hodnocení IMDb</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Datum přehrání</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Délka</translation>
</message>
<message>
<source>Ended at</source>
<translation>Skončilo v</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Nepodařilo se získat informace o přehrávání</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Go to season</source>
<translation>Přejít k sérii</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Season Page</extracomment>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>Můžete hledat názvy, osoby, kanály živého vysílání a mnohem více</translation>
<extracomment>Help text in search results</extracomment>
</message>
<message>
<source>Quick Connect</source>
<translation>Rychle připojit</translation>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Zde je Váš kód pro rychlé připojení:</translation>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(Dialog se automaticky zavře)</translation>
</message>
<message>
<source>all</source>
<translation>vše</translation>
<extracomment>all will reset the searchTerm so all data will be availible</extracomment>
</message>
<message>
<source>Slideshow On</source>
<translation>Slideshow zapnuto</translation>
</message>
<message>
<source>Random On</source>
<translation>Náhodné zapnout</translation>
</message>
<message>
<source>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</source>
<translation>Pomocí tlačítka pro přehrávání se můžete pomalu pohybovat k první položce ve složce. (Pokud je vypnuto, složka se znovu hned nastaví na první položku).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Skrýt text sloganu na stránce s podrobnostmi.</translation>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Rozmazat neshlédnuté epizody</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s home background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Použijte vygenerovaný obrázek úvodní obrazovky jako pozadí domovské stránky Jellyfin. Aby se změna projevila, bude třeba Jellyfin zavřít a znovu otevřít.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Režim Cinema přináší zážitek z kina přímo do vašeho obývacího pokoje díky možnosti přehrávání vlastních úvodů před hlavním titulem.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Skrýt všechny hodiny v Jellyfinu. Aby se změna projevila, je potřeba zavřít a znovu otevřít Jellyfin.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Zadejte název serveru nebo IP adresu</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Tato %1 neobsahuje žádné položky</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Datum přidání</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Rodičovské hodnocení</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Pohled</translation>
</message>
<message>
<source>Born</source>
<translation>Narozen</translation>
</message>
<message>
<source>Died</source>
<translation>Úmrtí</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Obsazení &amp; štáb</translation>
</message>
<message>
<source>Tuesday</source>
<translation>Úterý</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>Při přehrávání této položky došlo k chybě. Server neposkytl požadovaná data pro překódování.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</source>
<translation>**EXPERIMENTÁLNÍ** Podpora přímého přehrávání obsahu AV1, pokud to toto zařízení Roku podporuje.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>direct</source>
<translation>přímý</translation>
</message>
<message>
<source>Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).</source>
<translation>Vždy zobrazovat názvy pod obrázky plakátů. (Pokud je zakázáno, název se zobrazí pouze pod zvýrazněnou položkou).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Hide Clock</source>
<translation>Skrýt hodiny</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Dříve než selže překódování pokusit se o přímé přehrání H.264 média s nepodporovanými úrovněmi profilu.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložit přihlašovací údaje?</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Datum vydání</translation>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</source>
<translation>Dříve než selže překódování pokusit se o přímé přehrání HEVC média s nepodporovanými úrovněmi profilu.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Options for Home Page.</source>
<translation>Možností domovské stránky.</translation>
<extracomment>Description for Home Page user settings.</extracomment>
</message>
<message>
<source>Set the maximum amount of days a show should stay in the &apos;Next Up&apos; list without watching it.</source>
<translation>Nastavení maximálního počtu dní, pro setrvání pořadu v seznamu &quot;Další díly&quot;, aniž by byl zhlédnut.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Slideshow Off</source>
<translation>Slideshow vypnuto</translation>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Podpora přímého přehrávání obsahu MPEG-4. Toto může být nutné deaktivovat pro přehrávání videí kódovaných v DIVX.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>Podpora MPEG-4</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Reason</source>
<translation>Důvod</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Informace o překódování</translation>
</message>
<message>
<source>Cinema Mode</source>
<translation>Režim Cinema</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Use Splashscreen as Home Background</source>
<translation>Použít Splashscreen jako pozadí domovské stránky</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Honocení kritiků</translation>
</message>
<message>
<source>More Like This</source>
<translation>Více podobných</translation>
</message>
<message>
<source>Record</source>
<translation>Nahrávat</translation>
</message>
<message>
<source>Playback</source>
<translation>Přehrávání</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>User Interface</source>
<translation>Uživatelské prostředí</translation>
<extracomment>Title for User Interface section in user setting screen.</extracomment>
</message>
<message>
<source>Enabled</source>
<translation>Povoleno</translation>
</message>
<message>
<source>Studios</source>
<translation>Studia</translation>
</message>
<message>
<source>Age</source>
<translation>Věk</translation>
</message>
<message>
<source>On Now</source>
<translation>Nyní</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Smazat uložené</translation>
</message>
<message>
<source>Networks</source>
<translation>Sítě</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Počet přehrání</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Pro zavření stiskni &apos;OK&apos;</translation>
</message>
<message>
<source>Special Features</source>
<translation>Speciální funkce</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Další části</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>Movies</source>
<translation>FIlmy</translation>
</message>
<message>
<source>Started</source>
<translation>Začalo</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Začíná v</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Začíná</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Channels</source>
<translation>Kanály</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Programový průvodce</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Record Series</source>
<translation>Nahrávat řady</translation>
</message>
<message>
<source>Not found</source>
<translation>Nenalezeno</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Unknown</source>
<translation>Neznámý</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Vyberte server Jellyfin z místní sítě:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>MPEG-2 Support</source>
<translation>Podpora MPEG-2</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</source>
<translation>Podpora přímého přehrávání obsahu MPEG-2 (např. živé TV). To zabrání překódování obsahu MPEG-2, ale využívá podstatně větší šířku pásma.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>AV1 Support</source>
<translation>Podpora AV1</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Media Grid</source>
<translation>Mřížka s médii</translation>
<extracomment>UI -&gt; Media Grid section in user setting screen.</extracomment>
</message>
<message>
<source>Item Titles</source>
<translation>Názvy položek</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Media Grid options.</source>
<translation>Volby pro mřížku s médii.</translation>
</message>
<message>
<source>Item Count</source>
<translation>Počet položek</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Count in user setting screen.</extracomment>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Zobrazit počet položek v knihovně a ukazatel označených položek.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Set Watched</source>
<translation>Označit jako shlédnuté</translation>
<extracomment>Button Text - When pressed, marks item as Warched</extracomment>
</message>
<message>
<source>Set Favorite</source>
<translation>Nastavit jako oblíbené</translation>
<extracomment>Button Text - When pressed, sets item as Favorite</extracomment>
</message>
<message>
<source>Go to episode</source>
<translation>Přejít k epizodě</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Episode Detail Page</extracomment>
</message>
<message>
<source>Use voice remote to search</source>
<translation>K hledání použít hlasové ovládání</translation>
<extracomment>Help text in search voice text box</extracomment>
</message>
<message>
<source>Search now</source>
<translation>Hledat teď</translation>
<extracomment>Help text in search Box</extracomment>
</message>
<message>
<source>There was an error authenticating via Quick Connect.</source>
<translation>Při ověřování přes rychlé připojení se vyskytla chyba.</translation>
</message>
<message>
<source>Shows</source>
<translation>Pořady</translation>
</message>
<message>
<source>Return to Top</source>
<translation>Návrat nahoru</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Options for Details pages.</source>
<translation>Možnosti pro stránky s podrobnostmi.</translation>
<extracomment>Description for Details page user settings.</extracomment>
</message>
<message>
<source>Hide Taglines</source>
<translation>Skrýt slogany</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Předvolby pro TV pořady.</translation>
<extracomment>Description for TV Shows user settings.</extracomment>
</message>
<message>
<source>If enabled, images of unwatched episodes will be blurred.</source>
<translation>Pokud je povoleno, obrázky nezobrazených epizod budou zobrazeny rozmazaně.</translation>
</message>
<message>
<source>Screensaver</source>
<translation>Spořič obrazovky</translation>
</message>
<message>
<source>Options for Jellyfin&apos;s screensaver.</source>
<translation>Možnosti spořiče obrazovky Jellyfin.</translation>
<extracomment>Description for Screensaver user settings.</extracomment>
</message>
<message>
<source>Use Splashscreen as Screensaver Background</source>
<translation>Použít Splashscreen jako pozadí spořiče obrazovky</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Použijte vygenerovaný obrázek úvodní obrazovky jako pozadí spořiče obrazovky Jellyfin. Aby se změna projevila, bude třeba Jellyfin zavřít a znovu otevřít.</translation>
</message>
<message>
<source>Design Elements</source>
<translation>Prvky vzhledu</translation>
</message>
<message>
<source>Options that alter the design of Jellyfin.</source>
<translation>Možnosti, které mění vzhled Jellyfin.</translation>
<extracomment>Description for Design Elements user settings.</extracomment>
</message>
<message>
<source>Next episode</source>
<translation>Další epizoda</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Přehrát trailer</translation>
</message>
<message>
<source>Direct Play H.264 Unsupported Profile Levels</source>
<translation>Přímé přehrávání nepodporovaných úrovní profilu H.264</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Direct Play HEVC Unsupported Profile Levels</source>
<translation>Přímé přehrání nepodporovaných úrovní profilu HEVC</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Nastavení, která se týkají přehrávání, podporovanému kodeku a typům médií.</translation>
</message>
<message>
<source>Settings relating to how the application looks.</source>
<translation>Nastavení související se vzhledem aplikace.</translation>
</message>
<message>
<source>Home Page</source>
<translation>Domovská stránka</translation>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Maximum dní pro další díly</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Total Bitrate</source>
<translation>Celkový datový tok</translation>
</message>
<message>
<source>Playback Information</source>
<translation>Informace o přehrávání</translation>
</message>
<message>
<source>Audio Codec</source>
<translation>Audio kodek</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Informace o streamu</translation>
</message>
<message>
<source>Codec</source>
<translation>Kodek</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Tag kodeku</translation>
</message>
<message>
<source>Container</source>
<translation>Kontejner</translation>
<extracomment>Video streaming container</extracomment>
</message>
<message>
<source>Size</source>
<translation>Velikost</translation>
<extracomment>Video size</extracomment>
</message>
<message>
<source>Video range type</source>
<translation>Typ rozsahu videa</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Formát pixelu</translation>
<extracomment>Video pixel format</extracomment>
</message>
<message>
<source>WxH</source>
<translation>ŠxV</translation>
<extracomment>Video width x height</extracomment>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Slideshow obnoveno</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Slideshow pozastaveno</translation>
</message>
<message>
<source>Random Off</source>
<translation>Náhodné vypnout</translation>
</message>
<message>
<source>%1 of %2</source>
<translation>%1 z %2</translation>
<extracomment>Item position and count. %1 = current item. %2 = total number of items</extracomment>
</message>
<message>
<source>Go to series</source>
<translation>Přejít k seriálům</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Series Detail Page</extracomment>
</message>
<message>
<source>Details Page</source>
<translation>Stránka s podrobnostmi</translation>
</message>
<message>
<source>Video Codec</source>
<translation>Video kodek</translation>
</message>
<message>
<source>Version</source>
<translation>Verze</translation>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>nebo zadejte URL serveru ručně:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Požadovaný obsah na serveru neexistuje</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Close</source>
<translation>Zavřít</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Třídit</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filtr</translation>
</message>
<message>
<source>Thursday</source>
<translation>Čtvrtek</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Pátek</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Live</source>
<translation>Živě</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Skončilo v</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Opakovat</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Connecting to Server</source>
<translation>Připojování k serveru</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Disabled</source>
<translation>Zakázáno</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Zrušit nahrávání sérií</translation>
</message>
<message>
<source>Cancel Recording</source>
<translation>Zrušit nahrávání</translation>
</message>
<message>
<source>View Channel</source>
<translation>Zobrazit kanál</translation>
</message>
<message>
<source>Sunday</source>
<translation>Neděle</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Začátek v</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sobota</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Středa</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Pondělí</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>TV Shows</source>
<translation>TV pořady</translation>
</message>
<message>
<source>today</source>
<translation>dnes</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>včera</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>zítra</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Podpora přímého přehrávání obsahu MPEG-4. Toto může být nutné deaktivovat pro přehrávání videí kódovaných v DIVX.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>AV1</source>
<translation>AV1</translation>
<extracomment>Name of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>Delete Saved</source>
<translation>Smazat uložené</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložit přihlašovací údaje?</translation>
</message>
</context>
</TS>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -285,18 +285,33 @@
<source>More Like This</source>
<translation>More Like This</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
<message>
<source>Special Features</source>
<translation>Special Features</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>Additional Parts</source>
<translation>Additional Parts</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>Movies</source>
<translation>Movies</translation>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Movies (Presentation)</translation>
<extracomment>Movie library view option</extracomment>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Movies (Grid)</translation>
<extracomment>Movie library view option</extracomment>
</message>
<message>
<source>TV Shows</source>
<translation>TV Shows</translation>
@ -479,16 +494,47 @@
<translation>Playback</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>MPEG-2 Support</source>
<translation>MPEG-2 Support</translation>
<extracomment>Settings Menu - Title for option</extracomment>
<source>Codec Support</source>
<translation>Codec Support</translation>
<extracomment>Settings Menu - Title for settings group related to codec support</extracomment>
</message>
<message>
<source>Enable or disable Direct Play for optional codecs</source>
<translation>Enable or disable Direct Play for optional codecs</translation>
<extracomment>Settings Menu - Title for settings group related to codec support</extracomment>
</message>
<message>
<source>MPEG-2</source>
<translation>MPEG-2</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</source>
<translation>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>MPEG-4</source>
<translation>MPEG-4</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>AV1</source>
<translation>AV1</translation>
<extracomment>Name of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</source>
<translation>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</translation>
<extracomment>Description of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>Enabled</source>
<translation>Enabled</translation>
@ -642,6 +688,16 @@
<translation>Blur Unwatched Episodes</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Skip Details for Single Seasons</source>
<translation>Skip Details for Single Seasons</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</source>
<translation>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>If enabled, images of unwatched episodes will be blurred.</source>
<translation>If enabled, images of unwatched episodes will be blurred.</translation>
@ -703,10 +759,34 @@
<translation>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Next episode</source>
<translation>Next episode</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Play Trailer</translation>
</message>
<message>
<source>H.264</source>
<translation>H.264</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>HEVC</source>
<translation>HEVC</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</source>
<translation>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Settings relating to playback and supported codec and media types.</translation>
@ -817,5 +897,157 @@
<translation>Unable to find any albums or songs belonging to this artist</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Text Subtitles Only</translation>
<extracomment>Name of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>Only display text subtitles to minimize transcoding.</source>
<translation>Only display text subtitles to minimize transcoding.</translation>
<extracomment>Description of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>all</source>
<translation>all</translation>
<extracomment>all will reset the searchTerm so all data will be availible</extracomment>
</message>
<message>
<source>Aired</source>
<translation>Aired</translation>
<extracomment>Aired date label</extracomment>
</message>
<message>
<source>Slideshow Off</source>
<translation>Slideshow Off</translation>
</message>
<message>
<source>Slideshow On</source>
<translation>Slideshow On</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Slideshow Paused</translation>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Slideshow Resumed</translation>
</message>
<message>
<source>Random Off</source>
<translation>Random Off</translation>
</message>
<message>
<source>Random On</source>
<translation>Random On</translation>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>MPEG-4 Support</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Show What's New Popup</source>
<translation>Show What's New Popup</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Show What's New popup when Jellyfin is updated to a new version.</source>
<translation>Show What's New popup when Jellyfin is updated to a new version.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Unplayed</source>
<translation>Unplayed</translation>
</message>
<message>
<source>Played</source>
<translation>Played</translation>
</message>
<message>
<source>Resumable</source>
<translation>Resumable</translation>
</message>
<message>
<source>Movie Library Default View</source>
<translation>Movie Library Default View</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Default view for Movie Libraries.</source>
<translation>Default view for Movie Libraries.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Movies (Presentation)</translation>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Movies (Grid)</translation>
</message>
<message>
<source>Movie Library Grid Titles</source>
<translation>Movie Library Grid Titles</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Select when to show titles.</source>
<translation>Select when to show titles.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Show On Hover</source>
<translation>Show On Hover</translation>
</message>
<message>
<source>Always Show</source>
<translation>Always Show</translation>
</message>
<message>
<source>Always Hide</source>
<translation>Always Hide</translation>
</message>
<message>
<source>Artists (Presentation)</source>
<translation>Artists (Presentation)</translation>
</message>
<message>
<source>Artists (Grid)</source>
<translation>Artists (Grid)</translation>
</message>
<message>
<source>Song</source>
<translation>Song</translation>
</message>
<message>
<source>Songs</source>
<translation>Songs</translation>
</message>
<message>
<source>Album</source>
<translation>Album</translation>
</message>
<message>
<source>Albums</source>
<translation>Albums</translation>
</message>
<message>
<source>View All</source>
<translation>View All</translation>
</message>
<message>
<source>Disable Community Rating for Episodes</source>
<translation>Disable Community Rating for Episodes</translation>
</message>
<message>
<source>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</source>
<translation>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</translation>
</message>
</context>
</TS>

View File

@ -1614,5 +1614,104 @@
<source>Save Credentials?</source>
<translation>Guardar credenciales?</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Guardar Credenciales?</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error al recuperar contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Se ha encontrado un error mientras se reproducía este ítem.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Error al cargar datos del canal</translation>
</message>
<message>
<source>Movies</source>
<translation>Peliculas</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Programa de TV</translation>
</message>
<message>
<source>today</source>
<translation>Hoy</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>Ayer</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Error durante la reproducción</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Calificación de los Críticos</translation>
</message>
<message>
<source>Monday</source>
<translation>Lunes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Cargando información del canal</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Borrado confirmado</translation>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ocurrió un error al recuperar los datos para este ítem desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>No se pudo cargar los datos del canal desde el servidor</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene ítems</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Calificación IMDb</translation>
</message>
<message>
<source>Special Features</source>
<translation>Funciones especiales</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>tomorrow</source>
<translation>Mañana</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Domingo</translation>
<extracomment>Day of Week</extracomment>
</message>
</context>
</TS>

View File

@ -1328,5 +1328,22 @@
<source>Save Credentials?</source>
<translation>Guardar credenciales?</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>¿Guardar credenciales?</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Eliminar guardado</translation>
</message>
<message>
<source>On Now</source>
<translation>Ahora</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error al recuperar contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
</context>
</TS>

View File

@ -1884,5 +1884,816 @@
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Error de Reproducción de Contenido de Canal</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Terminar Sesión</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Borrar Credenciales</translation>
</message>
<message>
<source>Age</source>
<translation>Edad</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Criticas Raiting</translation>
</message>
<message>
<source>Born</source>
<translation>Nacido/a</translation>
</message>
<message>
<source>today</source>
<translation>hoy</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Reproduciendo Contenido de Canal</translation>
</message>
<message>
<source>Channels</source>
<translation>Canales</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>Died</source>
<translation>Muerto/a</translation>
</message>
<message>
<source>View Channel</source>
<translation>Ver Canales</translation>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>si no hay servidores disponibles, puedes agregar manualmente la URL</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ha ocurrido un error tratando de recuperar la información desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene elementos</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>IMDb Raiting</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Fecha Agregada</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Fecha Reproducida</translation>
</message>
<message>
<source>Movies</source>
<translation>Películas</translation>
</message>
<message>
<source>yesterday</source>
<translation>ayer</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>mañana</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Martes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Jueves</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Viernes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filtro</translation>
</message>
<message>
<source>Sunday</source>
<translation>Domingo</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Lunes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Miercoles</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sabado</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Comienza a</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>En Vivo</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repetir</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guia de Television</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Record</source>
<translation>Grabar</translation>
</message>
<message>
<source>Record Series</source>
<translation>Grabar Series</translation>
</message>
<message>
<source>Close</source>
<translation>Cerrar</translation>
</message>
<message>
<source>Unknown</source>
<translation>desconocido</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Connecting to Server</source>
<translation>Conectando con el Servidor</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Not found</source>
<translation>No se ha encontrado</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Guardar Credenciales?</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Ha ocurrido un error al reproducir este contenido.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error Recuperando Contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Padres Raiting</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambiar de Servidor</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Clasificar</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vista</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Tiempo de Ejecución</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Error Durante la Reproducción</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>No se ha podido reproducir el Contenido del Canal de este servidor</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Fecha de Premiere</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Cuenta de Reproducción</translation>
</message>
<message>
<source>More Like This</source>
<translation>Mas de este Estilo</translation>
</message>
<message>
<source>Special Features</source>
<translation>Funciones Especiales</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>TV Shows</source>
<translation>Programas de Televisión</translation>
</message>
<message>
<source>Ends at</source>
<translation>Termina a</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Cancel Recording</source>
<translation>Cancelar la grabación</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Cancelar la grabación de Series</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Elige un servidor Jellyfin disponible de la red local</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Started</source>
<translation>Comenzó</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Comenzó a</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Comienza</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Terminó</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Agregar el nombre del servidor o direccion de IP</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Elige un servidor Jellyfin disponible de la red local:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>si no hay servidores disponibles, puedes agregar manualmente la URL:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ha ocurrido un error tratando de recuperar la información desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Reproduciendo Contenido de Canal</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene elementos</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambiar Servidor</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Borrar Credenciales</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Puntuación de la crítica</translation>
</message>
<message>
<source>Age</source>
<translation>Edad</translation>
</message>
<message>
<source>today</source>
<translation>hoy</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>Died</source>
<translation>Muerto/a</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Puntuación de IMDb</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Reparto y equipo</translation>
</message>
<message>
<source>Saturday</source>
<translation>Sábado</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repetir</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Media Grid</source>
<translation>Cuadrícula de medios</translation>
<extracomment>UI -&gt; Media Grid section in user setting screen.</extracomment>
</message>
<message>
<source>Sign Out</source>
<translation>Cerrar sesión</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>¿Guardar credenciales?</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Error durante la reproducción</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Ha ocurrido un error al reproducir este contenido.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Reproduciendo contenido del canal</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Error de reproducción de contenido del canal</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Fecha en que se agregó</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Fecha de reproducción</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Control Parental</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Fecha de estreno</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Tiempo de duración</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vista</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Clasificar</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filtro</translation>
</message>
<message>
<source>Special Features</source>
<translation>Funciones especiales</translation>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>No se ha podido reproducir el contenido del canal de este servidor</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Conteo de reproducción</translation>
</message>
<message>
<source>On Now</source>
<translation>En directo ahora</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error recuperando contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ha ocurrido un error al tratar de recuperar la información de este contenido desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene elementos</translation>
</message>
<message>
<source>Born</source>
<translation>Nacido/a</translation>
</message>
<message>
<source>More Like This</source>
<translation>Mas de este estilo</translation>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>Se ha encontrado un error al reproducir este elemento. El servidor no proveyó la información necesaria para la transcodificación.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>Use voice remote to search</source>
<translation>Utilizar la búsqueda remota por voz</translation>
<extracomment>Help text in search voice text box</extracomment>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Películas (presentación)</translation>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Películas (cuadrícula)</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Programas de televisión</translation>
</message>
<message>
<source>yesterday</source>
<translation>ayer</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>mañana</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Jueves</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Viernes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Comenzó a las</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Comienza a las</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>En vivo</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guía de televisión</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Record Series</source>
<translation>Grabar serie</translation>
</message>
<message>
<source>Cancel Recording</source>
<translation>Cancelar la grabación</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Cancelar la grabación de la serie</translation>
</message>
<message>
<source>Close</source>
<translation>Cerrar</translation>
</message>
<message>
<source>Connecting to Server</source>
<translation>Conectando con el servidor</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Not found</source>
<translation>No se ha encontrado</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Unknown</source>
<translation>Desconocido</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>El contenido solicitado no existe en el servidor</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Agregar el nombre del servidor o su dirección de IP</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Elige un servidor Jellyfin disponible en la red local:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</source>
<translation>Soporte de reproducción directa para contenido MPEG-2 (ej., televisión en vivo). Esto previene la transcodificación de contenido MPEG-2, pero a mayor uso de ancho de banda.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Soporte de reproducción directa para contenido MPEG-4. Esto podría requerir ser deshabilitado para poder reproducir los archivos de video con encodificación DIVX.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</source>
<translation>**EXPERIMENTAL** Soporte de reproducción directa para contenido AV1 si este dispositivo Roku es compatible.</translation>
<extracomment>Description of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).</source>
<translation>Siempre mostrar los títulos por debajo de las imágenes de cartelera. (Si se deshabilita, el título se mostrará debajo del elemento resaltado solamente).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Item Count</source>
<translation>Conteo de elementos</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Count in user setting screen.</extracomment>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Mostrar el conteo de elementos en la biblioteca y en el índice del elemento seleccionado.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Set Favorite</source>
<translation>Agregar a favoritos</translation>
<extracomment>Button Text - When pressed, sets item as Favorite</extracomment>
</message>
<message>
<source>Set Watched</source>
<translation>Marcar como visto</translation>
<extracomment>Button Text - When pressed, marks item as Warched</extracomment>
</message>
<message>
<source>Go to series</source>
<translation>Ir a serie</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Series Detail Page</extracomment>
</message>
<message>
<source>Go to season</source>
<translation>Ir a la temporada</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Season Page</extracomment>
</message>
<message>
<source>Go to episode</source>
<translation>Ir al episodio</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Episode Detail Page</extracomment>
</message>
<message>
<source>Search now</source>
<translation>Buscar ahora</translation>
<extracomment>Help text in search Box</extracomment>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Presiona &apos;OK&apos; para cerrar</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Partes adicionales</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>Movies</source>
<translation>Películas</translation>
</message>
<message>
<source>Sunday</source>
<translation>Domingo</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Lunes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Martes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Miércoles</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Starts</source>
<translation>Comienza a las</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Terminó a las</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Termina a las</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Channels</source>
<translation>Canales</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Ver Canal</translation>
</message>
<message>
<source>Record</source>
<translation>Grabar</translation>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Si no hay servidores disponibles, puedes agregar manualmente la URL:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Error obteniendo la Información de reproducción</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Version</source>
<translation>Versión</translation>
</message>
<message>
<source>Playback</source>
<translation>Reproducción</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>User Interface</source>
<translation>Interfaz de usuario</translation>
<extracomment>Title for User Interface section in user setting screen.</extracomment>
</message>
<message>
<source>Media Grid options.</source>
<translation>Opciones de la cuadrícula de medios.</translation>
</message>
<message>
<source>Codec Support</source>
<translation>Soporte de Codec</translation>
<extracomment>Settings Menu - Title for settings group related to codec support</extracomment>
</message>
<message>
<source>Enable or disable Direct Play for optional codecs</source>
<translation>Habilitar o desactivar la reproducción directa para codecs opcionales</translation>
<extracomment>Settings Menu - Title for settings group related to codec support</extracomment>
</message>
<message>
<source>MPEG-2</source>
<translation>MPEG-2</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>MPEG-4</source>
<translation>MPEG-4</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>AV1</source>
<translation>AV1</translation>
<extracomment>Name of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>Item Titles</source>
<translation>Títulos de elementos</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Started</source>
<translation>Comenzó a las</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Enabled</source>
<translation>Activado</translation>
</message>
<message>
<source>Disabled</source>
<translation>Desactivado</translation>
</message>
<message>
<source>Shows</source>
<translation>espectáculos</translation>
</message>
<message>
<source>Quick Connect</source>
<translation>Conexión rápida</translation>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(El diálogo se cerrará automáticamente)</translation>
</message>
<message>
<source>Return to Top</source>
<translation>Vuelva a la parte superior</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>Puede buscar títulos, personas, canales de TV en vivo y más</translation>
<extracomment>Help text in search results</extracomment>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Aquí está su código de conexión rápida:</translation>
</message>
<message>
<source>There was an error authenticating via Quick Connect.</source>
<translation>Hubo un error al autenticarse a través de Quick Connect.</translation>
</message>
<message>
<source>Networks</source>
<translation>Redes</translation>
</message>
<message>
<source>Studios</source>
<translation>Estudios</translation>
</message>
<message>
<source>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</source>
<translation>Use el botón de reproducción para animar lentamente al primer elemento de la carpeta. (Si está deshabilitado, la carpeta se restablecerá al primer elemento inmediatamente).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
</context>
</TS>

File diff suppressed because it is too large Load Diff

View File

@ -1124,5 +1124,53 @@
<source>Change Server</source>
<translation>Changer de serveur</translation>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Chargement des données de la chaîne</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Erreur lors du chargement des données de la chaîne</translation>
</message>
<message>
<source>On Now</source>
<translation>En ce moment</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Erreur lors de la récupération du contenu</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Une erreur s&apos;est produite lors de la lecture de cet élément.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Change Server</source>
<translation>Changer de serveur</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Se déconnecter</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Sauvegarder les informations d&apos;authentification?</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Supprimer les valeurs enregistrées</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Erreur lors de la lecture</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Une erreur s&apos;est produite lors de la récupération des données de cet élément depuis le serveur.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
</context>
</TS>

File diff suppressed because it is too large Load Diff

View File

@ -1718,5 +1718,239 @@
<source>TAB_FILTER</source>
<translation>Filtro</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>È stato riscontrato un errore durante la riproduzione di questo oggetto.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Cast &amp; Crew</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Serie TV</translation>
</message>
<message>
<source>Wednesday</source>
<translation>Mercoledi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Giovedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started</source>
<translation>Iniziato</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Inizia</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Termina alle</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guida TV</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Connecting to Server</source>
<translation>In Connessione al Server</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Il contenuto richiesto non esiste sul server</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Inserisci il nome o l&apos;IP del server</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Scegli un server Jellyfin dalla rete locale</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Se il server non è nella lista, puoi anche inserire l&apos;URL:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Errore nel recupero delle informazioni di riproduzione</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Martedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>today</source>
<translation>oggi</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>Close</source>
<translation>Chiudi</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Premi &quot;OK&quot; per Chiudere</translation>
</message>
<message>
<source>Movies</source>
<translation>Film</translation>
</message>
<message>
<source>yesterday</source>
<translation>ieri</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>domani</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Domenica</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Lunedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Venerdi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sabato</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Iniziato il</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Inizia il</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Cancel Recording</source>
<translation>Interrompi Registrazione</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Interrompi Registrazione Seria</translation>
</message>
<message>
<source>Not found</source>
<translation>Non trovato</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Terminato alle</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>In diretta</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Replica</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Canali</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Visione del Canale</translation>
</message>
<message>
<source>Record</source>
<translation>Registra</translation>
</message>
<message>
<source>Unknown</source>
<translation>Sconosciuto</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Controllo Parentale</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Scegli un server Jellyfin dalla rete locale:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvare le credenziali?</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambia server</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Errore durante la riproduzione</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Errore durante la riproduzione</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>Sign Out</source>
<translation>Esci</translation>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>C&apos;è stato un errore nel recupero dei dati per questo elemento dal server.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>È stato riscontrato un errore durante la riproduzione di questo oggetto.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Caricamento dati del canale</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvare le credenziali?</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambia server</translation>
</message>
</context>
</TS>

View File

@ -2553,5 +2553,143 @@ não contém itens</translation>
<source>Change Server</source>
<translation>Alterar servidor</translation>
</message>
<message>
<source>Change Server</source>
<translation>Mudar Servidor</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Sair</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvar Credenciais?</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Deletar Salvos</translation>
</message>
<message>
<source>On Now</source>
<translation>Passando Agora</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Erro ao Carregar Conteúdo</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Erro Durante Reprodução</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Houve um erro ao coletar dados do servidor para este item.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Carregando dados do canal</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 não possui itens</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nome</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Data de Reprodução</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Avaliação IMDb</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Data de Lançamento</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Um erro foi encontrado enquanto reproduzindo este item.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Erro ao carregar os dados do canal</translation>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>Não foi possível carregar do servidor os dados do canal</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Avaliação de críticos</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Data de Adição</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Número de Reproduções</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Classificação Etária</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Sair</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvar Credenciais?</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Deletar Salvos</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Erro Durante a Reprodução</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>Change Server</source>
<translation>Mudar Servidor</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Erro ao Carregar Conteúdo</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Carregando Dados do Canal</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Foi encontrado um erro na reprodução deste item.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>On Now</source>
<translation>Em Exibição</translation>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Houve um erro ao recuperar os dados deste item do servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Erro ao carregar os Dados do Canal</translation>
</message>
</context>
</TS>

View File

@ -94,7 +94,7 @@
</message>
<message>
<source>My Media</source>
<translation>Fișierele mele Media</translation>
<translation>Librăria Mea</translation>
</message>
<message>
<source>Continue Watching</source>
@ -1859,5 +1859,751 @@
<translation>Suport pentru redarea directă a conținutului MPEG 2 (ex. Televiziune în direct). Conținutul MPEG 2 nu va fi transcodat, dar se va folosi significativ mai multă lățime de bandă</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Delete Saved</source>
<translation>Şterge Salvarea</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvezi Credenţialele ?</translation>
</message>
<message>
<source>On Now</source>
<translation>Chiar Acum</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Scor IMDb</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Data Redării</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Data Adăugării</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Timp de Funcţionare</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Sortare</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filtru</translation>
</message>
<message>
<source>today</source>
<translation>astăzi</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>ieri</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Not found</source>
<translation>Nu a fost găsit</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Eroare În Timpul Redării</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>A existat o eroare la redarea datelor pentru acest element de pe server.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Data Lansării</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vedere</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Seriale TV</translation>
</message>
<message>
<source>tomorrow</source>
<translation>mâine</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Începe la</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>S-a terminat la</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Channels</source>
<translation>Canale</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>Record Series</source>
<translation>Înregistrează Serial</translation>
</message>
<message>
<source>Cancel Recording</source>
<translation>Anulează înregistrarea</translation>
</message>
<message>
<source>Close</source>
<translation>Închide</translation>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Eroare la Primirea Informației despre conținutul selectat</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Started</source>
<translation>A început</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Se termină la</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>În Direct</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>O eroare a fost întâlnită în timpul redării acestui element. Serverul nu a furnizat datele de transcodare necesare.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</source>
<translation>Sprijin Direct Play de conținut MPEG-2 (de exemplu, Live TV). Acest lucru va preveni transcodarea conținutului MPEG-2, dar utilizează semnificativ mai multă lățime de bandă.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>User Interface</source>
<translation>Interfață Utilizator</translation>
<extracomment>Title for User Interface section in user setting screen.</extracomment>
</message>
<message>
<source>Item Count</source>
<translation>Număr Elemente</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Count in user setting screen.</extracomment>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>Puteți căuta titluri, persoane, canale TV în direct și multe altele</translation>
<extracomment>Help text in search results</extracomment>
</message>
<message>
<source>Quick Connect</source>
<translation>Conectare Rapidă</translation>
</message>
<message>
<source>Studios</source>
<translation>Studiouri</translation>
</message>
<message>
<source>Shows</source>
<translation>Seriale</translation>
</message>
<message>
<source>Screensaver</source>
<translation>Screensaver</translation>
</message>
<message>
<source>Set Favorite</source>
<translation>Setare ca Favorit</translation>
<extracomment>Button Text - When pressed, sets item as Favorite</extracomment>
</message>
<message>
<source>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</source>
<translation>Utilizați butonul de reluare pentru a anima încet la primul element din folder. (Dacă este dezactivat, folderul se va reseta imediat la primul element).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Opțiuni pentru seriale TV.</translation>
<extracomment>Description for TV Shows user settings.</extracomment>
</message>
<message>
<source>Options for Jellyfin&apos;s screensaver.</source>
<translation>Opțiuni pentru Jellyfin screensaver.</translation>
<extracomment>Description for Screensaver user settings.</extracomment>
</message>
<message>
<source>Use Splashscreen as Screensaver Background</source>
<translation>Utilizarea Splashscreen ca fundal Screensaver</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Utilizați imaginea generată pe splashscreen ca fundal de ecran Jellyfin . Jellyfin va trebui fie închisă și redeschisă pentru ca schimbarea intre în vigoare.</translation>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s home background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Utilizați imaginea generată pe splashscreen ca fundal de pornire Jellyfin. Jellyfin va trebui fie închisă și redeschisă pentru ca schimbarea intre în vigoare.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Ascunde toate ceasurile în Jellyfin. Jellyfin va trebui fie închisă și redeschisă pentru ca schimbarea intre în vigoare.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</source>
<translation>Încercați redarea directă pentru mediu HEVC cu niveluri de profil neacceptate înainte de a reveni la trancodare dacă nu reușește.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Options for Home Page.</source>
<translation>Opţiuni pentru Pagină Principală.</translation>
<extracomment>Description for Home Page user settings.</extracomment>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Numai subtitrări de text</translation>
<extracomment>Name of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>Only display text subtitles to minimize transcoding.</source>
<translation>Afișați numai subtitrări de text pentru a minimiza transcodarea.</translation>
<extracomment>Description of a setting - should we hide subtitles that might transcode</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Suport Direct Play de conținut MPEG-4. Acest lucru poate fi necesar fie dezactivat pentru redarea fișierelor video codificate DIVX.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Started at</source>
<translation>A Început la</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Începe</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Introdu Numele Serverului sau Adresa IP</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Hide Taglines</source>
<translation>Ascunde tag-urile</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Am întâmpinat o eroare în timpul redării articolului</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Scorul Criticilor</translation>
</message>
<message>
<source>Connecting to Server</source>
<translation>Se conectează la server</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(Dialogul se va închide automat)</translation>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Ascunde textul tagline-ului în paginile cu detalii.</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Eroare La Afişarea Conținutului</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>Born</source>
<translation>Nascut</translation>
</message>
<message>
<source>Died</source>
<translation>Decedat</translation>
</message>
<message>
<source>More Like This</source>
<translation>Mai Multe Ca şi Acesta</translation>
</message>
<message>
<source>TV Guide</source>
<translation>Ghid TV</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Record</source>
<translation>Înregistrează</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Anulează Înregistrarea Serialului</translation>
</message>
<message>
<source>MPEG-2 Support</source>
<translation>MPEG-2 Sprijin</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Media Grid options.</source>
<translation>Opţiuni Grilă Media.</translation>
</message>
<message>
<source>Item Titles</source>
<translation>Titluri Conținut</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).</source>
<translation>Afișați întotdeauna titlurile de sub imaginile posterului. (Dacă este dezactivat, titlul va fi afișat numai sub elementul evidențiat).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Afișați numărul de elemente din bibliotecă și indexul elementului selectat.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Use voice remote to search</source>
<translation>Utilizarea telecomenzii vocale pentru a căuta</translation>
<extracomment>Help text in search voice text box</extracomment>
</message>
<message>
<source>%1 of %2</source>
<translation>1% din 2%</translation>
<extracomment>Item position and count. %1 = current item. %2 = total number of items</extracomment>
</message>
<message>
<source>There was an error authenticating via Quick Connect.</source>
<translation>S-a întâmpinat o eroare în timpul Conectării Rapide.</translation>
</message>
<message>
<source>Networks</source>
<translation>Rețele</translation>
</message>
<message>
<source>Return to Top</source>
<translation>Întoarce-te sus</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Încețoșeaza Episoadele Nevizualizate</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Design Elements</source>
<translation>Elemente design</translation>
</message>
<message>
<source>Cinema Mode</source>
<translation>Modul Cinema</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Hide Clock</source>
<translation>Ascunde Ceas</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Bit Rate</source>
<translation>Bit Rate</translation>
<extracomment>Video streaming bit rate</extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Vezi Canal</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Eroare la Încărcarea Datelor Canalului</translation>
</message>
<message>
<source>Age</source>
<translation>Vârstă</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Distribuţie &amp; Echipă</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Informații despre Stream</translation>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>Imposibili de găsit orice albume sau melodii aparținând acestui artist</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Se Încarcă Datele Canalului</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Număr Redări</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Apasă &apos;OK&apos; Pentru a Închide</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Părţi Adiţionale</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>Movies</source>
<translation>Filme</translation>
</message>
<message>
<source>Sunday</source>
<translation>Duminică</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Luni</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Marți</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Miercuri</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Vineri</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sâmbătă</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Redifuzare</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Conținutul cerut nu există pe server</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Dacă niciun server nu este arătat mai jos , atunci adăugați URL-ul serverului manual:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>Version</source>
<translation>Versiunea</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Selectați un server Jellyfin disponibil din rețeaua locală:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>AV1 Support</source>
<translation>Suport AV1</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</source>
<translation>** EXPERIMENTAL** Suport redare directă a conținutului AV1 dacă acest dispozitiv Roku îl acceptă.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Disabled</source>
<translation>Oprit</translation>
</message>
<message>
<source>Media Grid</source>
<translation>Grilă media</translation>
<extracomment>UI -&gt; Media Grid section in user setting screen.</extracomment>
</message>
<message>
<source>Set Watched</source>
<translation>Setare ca Vizionat</translation>
<extracomment>Button Text - When pressed, marks item as Warched</extracomment>
</message>
<message>
<source>Go to series</source>
<translation>Du-te la Seriale</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Series Detail Page</extracomment>
</message>
<message>
<source>Go to season</source>
<translation>De Sezon</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Season Page</extracomment>
</message>
<message>
<source>Go to episode</source>
<translation>Du-te la Episoade</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Episode Detail Page</extracomment>
</message>
<message>
<source>Search now</source>
<translation>Caută acum</translation>
<extracomment>Help text in search Box</extracomment>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Acesta este codul dumneavoastră de Conectare Rapidă:</translation>
</message>
<message>
<source>Details Page</source>
<translation>Pagină cu Detalii</translation>
</message>
<message>
<source>Options for Details pages.</source>
<translation>Opţiuni pentru Pagină cu Detalii.</translation>
<extracomment>Description for Details page user settings.</extracomment>
</message>
<message>
<source>If enabled, images of unwatched episodes will be blurred.</source>
<translation>Dacă este activată, imaginile episoadelor nevizionate vor fi înceţoşate.</translation>
</message>
<message>
<source>Options that alter the design of Jellyfin.</source>
<translation>Opțiuni ce modifică designul Jellyfin.</translation>
<extracomment>Description for Design Elements user settings.</extracomment>
</message>
<message>
<source>Use Splashscreen as Home Background</source>
<translation>Foloseşte Splashscreen ca Imagine de Fundal pentru Acasa</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Modul Cinema aduce experiența de teatru direct în camera de zi, cu posibilitatea de a reda intr-o-uri personalizate înainte de caracteristica principală.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Next episode</source>
<translation>Episodul urmator</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Porneste Trailer</translation>
</message>
<message>
<source>Direct Play H.264 Unsupported Profile Levels</source>
<translation>Redare directă H.264 Niveluri de profil neacceptate</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Încercați redare directă pentru suportul media H.264 cu niveluri de profil neacceptate înainte de a reveni la transcodare dacă nu reușește.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Direct Play HEVC Unsupported Profile Levels</source>
<translation>Redare Directă niveluri de profil HEVC Neacceptate</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Setări referitoare la redare și tipuri de codecuri suportate si acceptate.</translation>
</message>
<message>
<source>Settings relating to how the application looks.</source>
<translation>Setări referitoare la modul în care arată aplicația.</translation>
</message>
<message>
<source>Home Page</source>
<translation>Pagină Principală</translation>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Zilele maxime ce vor urma</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Set the maximum amount of days a show should stay in the &apos;Next Up&apos; list without watching it.</source>
<translation>Setați numărul maxim de zile în care o emisiune ar trebui rămână în lista &quot;Next Up&quot; fără a o urmări.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Playback Information</source>
<translation>Informații de redare</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Informații de transcodare</translation>
</message>
<message>
<source>Reason</source>
<translation>Motiv</translation>
</message>
<message>
<source>Video Codec</source>
<translation>Video Codec</translation>
</message>
<message>
<source>Audio Codec</source>
<translation>Audio Codec</translation>
</message>
<message>
<source>direct</source>
<translation>direct</translation>
</message>
<message>
<source>Total Bitrate</source>
<translation>Total Bitrate</translation>
</message>
<message>
<source>Audio Channels</source>
<translation>Canale Audio</translation>
</message>
<message>
<source>Codec</source>
<translation>Codec</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Codec Tag</translation>
</message>
<message>
<source>Level</source>
<translation>Nivel</translation>
<extracomment>Video profile level</extracomment>
</message>
<message>
<source>Container</source>
<translation>Container</translation>
<extracomment>Video streaming container</extracomment>
</message>
<message>
<source>Size</source>
<translation>Mărime</translation>
<extracomment>Video size</extracomment>
</message>
<message>
<source>Video range type</source>
<translation>Tipul intervalului video</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Formatul pixelului</translation>
<extracomment>Video pixel format</extracomment>
</message>
<message>
<source>WxH</source>
<translation>WxH</translation>
<extracomment>Video width x height</extracomment>
</message>
<message>
<source>Aired</source>
<translation>Difuzat</translation>
<extracomment>Aired date label</extracomment>
</message>
<message>
<source>all</source>
<translation>tot</translation>
<extracomment>all will reset the searchTerm so all data will be availible</extracomment>
</message>
<message>
<source>Slideshow On</source>
<translation>Slideshow On</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Slideshow Paused</translation>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Slideshow Resumed</translation>
</message>
<message>
<source>Slideshow Off</source>
<translation>Slideshow Off</translation>
</message>
<message>
<source>Random Off</source>
<translation>Aleatoriu Oprit</translation>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>MPEG-4 Sprijin</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Random On</source>
<translation>Aleatoriu Pornit</translation>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>Nu se pot încărca Datele Canalului de pe server</translation>
</message>
<message>
<source>Thursday</source>
<translation>Joi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Acest 1% nu conţine articole</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nume</translation>
</message>
<message>
<source>Special Features</source>
<translation>Caracteristici Speciale</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Scor Parental</translation>
</message>
<message>
<source>Unknown</source>
<translation>Necunoscut</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Playback</source>
<translation>Redare conținut</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>Enabled</source>
<translation>Pornit</translation>
</message>
</context>
</TS>

View File

@ -671,5 +671,456 @@
<source>Save Credentials?</source>
<translation>Uložiť poverenia?</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložiť prihlasovacie údaje?</translation>
</message>
<message>
<source>Monday</source>
<translation>Pondelok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Začalo o</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Zobraziť kanál</translation>
</message>
<message>
<source>Record Series</source>
<translation>Séria nahrávok</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Zrušiť nahrávanie série</translation>
</message>
<message>
<source>Connecting to Server</source>
<translation>Pripája sa k serveru</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Zadajte názov servera alebo IP adresu</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Chyba pri získavaní informácií o prehrávaní</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>Pri prehrávaní tejto položky sa vyskytla chyba. Server neposkytol požadované údaje na prekódovanie.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>Live</source>
<translation>Naživo</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Piatok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Delete Saved</source>
<translation>Odstrániť uložené</translation>
</message>
<message>
<source>On Now</source>
<translation>Teraz</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Dátum hrania</translation>
</message>
<message>
<source>TV Guide</source>
<translation>TV sprievodca</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Born</source>
<translation>Narodený</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Herci &amp; štáb</translation>
</message>
<message>
<source>More Like This</source>
<translation>Viac takých</translation>
</message>
<message>
<source>Movies</source>
<translation>Filmy</translation>
</message>
<message>
<source>Tuesday</source>
<translation>Utorok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Starts</source>
<translation>Začína</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Skončil o</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Požadovaný obsah na serveri neexistuje</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Meno</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Počet prehrania</translation>
</message>
<message>
<source>Died</source>
<translation>Zomrel</translation>
</message>
<message>
<source>Age</source>
<translation>Vek</translation>
</message>
<message>
<source>Special Features</source>
<translation>Špeciálne vlastnosti</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Zatvorte stlačením tlačidla OK</translation>
</message>
<message>
<source>tomorrow</source>
<translation>zajtra</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Nedeľa</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Streda</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sobota</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started</source>
<translation>Začalo</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Začne</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Končí o</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Not found</source>
<translation>Nenájdené</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Triediť</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Beh programu</translation>
</message>
<message>
<source>Version</source>
<translation>Verzia</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Tento %1 neobsahuje žiadne položky</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>IMDB hodnotenie</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Dátum pridania</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Rodičovské hodnotenie</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Dátum vydania</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vyhliadka</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>TV Shows</source>
<translation>TV relácie</translation>
</message>
<message>
<source>today</source>
<translation>dnes</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>včera</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Štvrtok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Opakujte</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Kanály</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>Record</source>
<translation>Záznam</translation>
</message>
<message>
<source>Cancel Recording</source>
<translation>Zrušiť nahrávanie</translation>
</message>
<message>
<source>Close</source>
<translation>Zavrieť</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Vyberte dostupný server Jellyfin z vašej lokálnej siete:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Ak vyššie nie je uvedený žiadny server, adresu URL servera môžete zadať aj ručne:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Hodnotenie kritikov</translation>
</message>
<message>
<source>Unknown</source>
<translation>Neznámy</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Playback</source>
<translation>Prehrávanie</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Tento %1 neobsahuje žiadne položky</translation>
</message>
<message>
<source>Tuesday</source>
<translation>utorok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>piatok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Hodnotenie IMDb</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Dátum vydania</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Zobrazenie</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Uložiť prihlasovacie údaje?</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Dátum prehrania</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Obsadenie a štáb</translation>
</message>
<message>
<source>Movies</source>
<translation>Filmy</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Meno</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Odstrániť uložené</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Dátum pridania</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Rodičovské hodnotenie</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Počet prehraní</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Dĺžka</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Zoradenie</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>Born</source>
<translation>Dátum narodenia</translation>
</message>
<message>
<source>Died</source>
<translation>Dátum úmrtia</translation>
</message>
<message>
<source>Age</source>
<translation>Vek</translation>
</message>
<message>
<source>More Like This</source>
<translation>Viac podobných</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Hodnotenie kritikov</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Zatvorte stlačením tlačidla &apos;OK&apos;</translation>
</message>
<message>
<source>Special Features</source>
<translation>Špeciálne Funkcie</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Dodatočné Časti</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>včera</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>nedeľa</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>pondelok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>streda</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>štvrtok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>sobota</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Filmy (prezentácia)</translation>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Filmy (mriežka)</translation>
</message>
<message>
<source>TV Shows</source>
<translation>TV Seriály</translation>
</message>
<message>
<source>today</source>
<translation>dnes</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>zajtra</translation>
<extracomment>Next day</extracomment>
</message>
</context>
</TS>

View File

@ -1,8 +1,8 @@
## Channel Details
title=Jellyfin
major_version=1
minor_version=5
build_version=0
minor_version=6
build_version=3
### Main Menu Icons / Channel Poster Artwork
mm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png

7782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,12 @@
{
"name": "jellyfin-roku",
"version": "1.4.12",
"version": "1.6.3",
"description": "Roku app for Jellyfin media server",
"main": "index.js",
"directories": {
"test": "tests"
},
"devDependencies": {
"@rokucommunity/bslint": "0.7.5",
"brighterscript": "0.57.2",
"rooibos-cli": "1.4.0",
"ropm": "0.10.10"
"@rokucommunity/bslint": "0.8.1",
"brighterscript": "0.61.3",
"ropm": "0.10.11"
},
"scripts": {
"postinstall": "npx ropm copy",
@ -41,4 +37,4 @@
"sob": "npm:slide-out-button@^1.0.1",
"intKeyboard": "npm:integer-keyboard@^1.0.12"
}
}
}

View File

@ -4,18 +4,51 @@
"description": "Settings relating to playback and supported codec and media types.",
"children": [
{
"title": "MPEG-2 Support",
"description": "Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.",
"settingName": "playback.mpeg2",
"type": "bool",
"default": "false"
"title": "Codec Support",
"description": "Enable or disable Direct Play support for certain codecs",
"children": [
{
"title": "AV1",
"description": "** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.",
"settingName": "playback.av1",
"type": "bool",
"default": "false"
},
{
"title": "MPEG-2",
"description": "Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.",
"settingName": "playback.mpeg2",
"type": "bool",
"default": "false"
},
{
"title": "MPEG-4",
"description": "Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.",
"settingName": "playback.mpeg4",
"type": "bool",
"default": "true"
}
]
},
{
"title": "Attempt Direct Play (Profile Lvl)",
"description": "Attempt Direct Play for H.264 media with unsupported profile levels (> 4.2) before falling back to transcoding if it fails.",
"settingName": "playback.tryDirect.h264ProfileLevel",
"type": "bool",
"default": "true"
"title": "Profile Level Support",
"description": "Attempt Direct Play of potentially unsupported profile levels",
"children": [
{
"title": "H.264",
"description": "Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.",
"settingName": "playback.tryDirect.h264ProfileLevel",
"type": "bool",
"default": "true"
},
{
"title": "HEVC",
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.",
"settingName": "playback.tryDirect.hevcProfileLevel",
"type": "bool",
"default": "true"
}
]
},
{
"title": "Cinema Mode",
@ -23,9 +56,23 @@
"settingName": "playback.cinemamode",
"type": "bool",
"default": "false"
},
{
"title": "Text Subtitles Only",
"description": "Only display text subtitles to minimize transcoding.",
"settingName": "playback.subs.onlytext",
"type": "bool",
"default": "false"
}
]
},
{
"title": "Show What's New Popup",
"description": "Show What's New popup when Jellyfin is updated to a new version.",
"settingName": "load.allowwhatsnew",
"type": "bool",
"default": "true"
},
{
"title": "User Interface",
"description": "Settings relating to how the application looks.",
@ -40,6 +87,13 @@
"settingName": "ui.details.maxdaysnextup",
"type": "integer",
"default": "365"
},
{
"title": "Use Splashscreen as Home Background",
"description": "Use generated splashscreen image as Jellyfin's home background. Jellyfin will need to be closed and reopened for change to take effect.",
"settingName": "ui.home.splashBackground",
"type": "bool",
"default": "false"
}
]
},
@ -66,6 +120,20 @@
"settingName": "ui.tvshows.blurunwatched",
"type": "bool",
"default": "false"
},
{
"title": "Skip Details for Single Seasons",
"description": "If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.",
"settingName": "ui.tvshows.goStraightToEpisodeListing",
"type": "bool",
"default": "false"
},
{
"title":"Disable Community Rating for Episodes",
"description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
"settingName": "ui.tvshows.disableCommunityRating",
"type":"bool",
"default":"false"
}
]
},
@ -92,13 +160,6 @@
"settingName": "ui.design.hideclock",
"type": "bool",
"default": "false"
},
{
"title": "Use Splashscreen as Home Background",
"description": "Use generated splashscreen image as Jellyfin's home background. Jellyfin will need to be closed and reopened for change to take effect.",
"settingName": "ui.home.splashBackground",
"type": "bool",
"default": "false"
}
]
},
@ -106,6 +167,44 @@
"title": "Media Grid",
"description": "Media Grid options.",
"children": [
{
"title": "Movie Library Default View",
"description": "Default view for Movie Libraries.",
"settingName": "itemgrid.movieDefaultView",
"type": "radio",
"default": "movies",
"options": [
{
"title": "Movies (Presentation)",
"id": "Movies"
},
{
"title": "Movies (Grid)",
"id": "MoviesGrid"
}
]
},
{
"title": "Movie Library Grid Titles",
"description": "Select when to show titles.",
"settingName": "itemgrid.movieGridTitles",
"type": "radio",
"default": "showonhover",
"options": [
{
"title": "Show On Hover",
"id": "showonhover"
},
{
"title": "Always Show",
"id": "showalways"
},
{
"title": "Always Hide",
"id": "hidealways"
}
]
},
{
"title": "Item Count",
"description": "Show item count in the library and index of selected item.",

View File

@ -1,8 +1,6 @@
sub Main (args as dynamic) as void
' If the Rooibos files are included in deployment, run tests
'bs:disable-next-line
if type(Rooibos__Init) = "Function" then Rooibos__Init()
appInfo = CreateObject("roAppInfo")
' The main function that runs when the application is launched.
m.screen = CreateObject("roSGScreen")
@ -13,13 +11,6 @@ sub Main (args as dynamic) as void
WriteAsciiFile("tmp:/scene.temp", "")
MoveFile("tmp:/scene.temp", "tmp:/scene")
' Temporary code to migrate MPEG2 setting from device setting to user setting
' Added for 1.4.13 release and should probably be removed for 1.4.15
if get_setting("playback.mpeg2") <> invalid and registry_read("playback.mpeg2", get_setting("active_user")) = invalid
set_user_setting("playback.mpeg2", get_setting("playback.mpeg2"))
end if
' End Temporary code
m.port = CreateObject("roMessagePort")
m.screen.setMessagePort(m.port)
m.scene = m.screen.CreateScene("JFScene")
@ -34,6 +25,7 @@ sub Main (args as dynamic) as void
sceneManager = CreateObject("roSGNode", "SceneManager")
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
app_start:
' First thing to do is validate the ability to use the API
@ -49,6 +41,17 @@ sub Main (args as dynamic) as void
m.scene.observeField("exit", m.port)
' Only show the Whats New popup the first time a user runs a new client version.
if appInfo.GetVersion() <> get_setting("LastRunVersion")
' Ensure the user hasn't disabled Whats New popups
if get_user_setting("load.allowwhatsnew") = "true"
set_setting("LastRunVersion", appInfo.GetVersion())
dialog = createObject("roSGNode", "WhatsNewDialog")
m.scene.dialog = dialog
m.scene.dialog.observeField("buttonSelected", m.port)
end if
end if
' Handle input messages
input = CreateObject("roInput")
input.SetMessagePort(m.port)
@ -59,10 +62,10 @@ sub Main (args as dynamic) as void
m.device.EnableAppFocusEvent(false)
' Check if we were sent content to play with the startup command (Deep Link)
if (args.mediaType <> invalid) and (args.contentId <> invalid)
if isValidAndNotEmpty(args.mediaType) and isValidAndNotEmpty(args.contentId)
video = CreateVideoPlayerGroup(args.contentId)
if video <> invalid and video.errorMsg <> "introaborted"
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
else
dialog = createObject("roSGNode", "Dialog")
@ -94,6 +97,7 @@ sub Main (args as dynamic) as void
group.setFocus(true)
end if
else if isNodeEvent(msg, "quickPlayNode")
group = sceneManager.callFunc("getActiveScene")
reportingNode = msg.getRoSGNode()
itemNode = reportingNode.quickPlayNode
if itemNode = invalid or itemNode.id = "" then return
@ -106,12 +110,42 @@ sub Main (args as dynamic) as void
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
end if
if LCase(group.subtype()) = "tvepisodes"
if isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if
end if
reportingNode.quickPlayNode.type = ""
end if
else if isNodeEvent(msg, "selectedItem")
' If you select a library from ANYWHERE, follow this flow
selectedItem = msg.getData()
m.selectedItemType = selectedItem.type
if selectedItem.type = "CollectionFolder" or selectedItem.type = "UserView" or selectedItem.type = "Folder" or selectedItem.type = "Channel" or selectedItem.type = "Boxset"
if selectedItem.type = "CollectionFolder"
if selectedItem.collectionType = "movies"
group = CreateMovieLibraryView(selectedItem)
else if selectedItem.collectionType = "music"
group = CreateMusicLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "Genre"
' User clicked on a genre folder
if selectedItem.json.MovieCount > 0
group = CreateMovieLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "MusicGenre"
group = CreateMusicLibraryView(selectedItem)
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "UserView" or selectedItem.type = "Folder" or selectedItem.type = "Channel" or selectedItem.type = "Boxset"
group = CreateItemGrid(selectedItem)
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Episode"
@ -128,6 +162,8 @@ sub Main (args as dynamic) as void
end if
else if selectedItem.type = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json)
else if selectedItem.type = "Season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
else if selectedItem.type = "Movie"
' open movie detail page
group = CreateMovieDetailsGroup(selectedItem)
@ -142,7 +178,12 @@ sub Main (args as dynamic) as void
dialog.title = tr("Loading Channel Data")
m.scene.dialog = dialog
video = CreateVideoPlayerGroup(video_id)
if LCase(selectedItem.subtype()) = "extrasdata"
video = CreateVideoPlayerGroup(video_id, invalid, 1, false, true, false)
else
video = CreateVideoPlayerGroup(video_id)
end if
dialog.close = true
if video <> invalid and video.errorMsg <> "introaborted"
@ -166,7 +207,9 @@ sub Main (args as dynamic) as void
else if selectedItem.type = "MusicAlbum"
group = CreateAlbumView(selectedItem.json)
else if selectedItem.type = "Audio"
group = CreateAudioPlayerGroup([selectedItem.json])
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem.json)
m.global.queueManager.callFunc("playQueue")
else
' TODO - switch on more node types
message_dialog("This type is not yet supported: " + selectedItem.type + ".")
@ -202,17 +245,27 @@ sub Main (args as dynamic) as void
' User has selected audio they want us to play
selectedIndex = msg.getData()
screenContent = msg.getRoSGNode()
group = CreateAudioPlayerGroup([screenContent.albumData.items[selectedIndex]])
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playAllSelected")
' User has selected playlist of of audio they want us to play
screenContent = msg.getRoSGNode()
m.spinner = screenContent.findNode("spinner")
m.spinner.visible = true
group = CreateAudioPlayerGroup(screenContent.albumData.items)
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("set", screenContent.albumData.items)
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playArtistSelected")
' User has selected playlist of of audio they want us to play
screenContent = msg.getRoSGNode()
group = CreateArtistMixGroup(screenContent.pageContent.id)
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("set", CreateArtistMix(screenContent.pageContent.id).Items)
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "instantMixSelected")
' User has selected instant mix
' User has selected playlist of of audio they want us to play
@ -222,20 +275,26 @@ sub Main (args as dynamic) as void
m.spinner.visible = true
end if
group = invalid
viewHandled = false
' Create instant mix based on selected album
if isValid(screenContent.albumData)
if isValid(screenContent.albumData.items)
if screenContent.albumData.items.count() > 0
group = CreateInstantMixGroup(screenContent.albumData.items)
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.albumData.items[0].id).Items)
m.global.queueManager.callFunc("playQueue")
viewHandled = true
end if
end if
end if
' Create instant mix based on selected artist
if not isValid(group)
group = CreateInstantMixGroup([{ id: screenContent.pageContent.id }])
if not viewHandled
' Create instant mix based on selected artist
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.pageContent.id).Items)
m.global.queueManager.callFunc("playQueue")
end if
else if isNodeEvent(msg, "episodeSelected")
@ -280,7 +339,9 @@ sub Main (args as dynamic) as void
else if node.type = "MusicAlbum"
group = CreateAlbumView(node.json)
else if node.type = "Audio"
group = CreateAudioPlayerGroup([node.json])
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", node.json)
m.global.queueManager.callFunc("playQueue")
else if node.type = "Person"
group = CreatePersonView(node)
else if node.type = "TvChannel"
@ -292,7 +353,9 @@ sub Main (args as dynamic) as void
else if node.type = "Audio"
selectedIndex = msg.getData()
screenContent = msg.getRoSGNode()
group = CreateAudioPlayerGroup([screenContent.albumData.items[node.id]])
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", screenContent.albumData.items[node.id])
m.global.queueManager.callFunc("playQueue")
else
' TODO - switch on more node types
message_dialog("This type is not yet supported: " + node.type + ".")
@ -324,9 +387,17 @@ sub Main (args as dynamic) as void
dialog.close = true
end if
if group.lastfocus.id = "main_group"
buttons = group.findNode("buttons")
if isValid(buttons)
group.lastfocus = group.findNode("buttons")
end if
end if
if group.lastFocus <> invalid
group.lastFocus.setFocus(true)
end if
else if btn <> invalid and btn.id = "trailer-button"
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Loading trailer")
@ -339,7 +410,7 @@ sub Main (args as dynamic) as void
video_id = trailerData[0].id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx)
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
dialog.close = true
@ -428,7 +499,7 @@ sub Main (args as dynamic) as void
if m.selectedItemType = "TvChannel" and node.state = "finished"
video = CreateVideoPlayerGroup(node.id)
m.global.sceneManager.callFunc("pushScene", video)
m.global.sceneManager.callFunc("clearPreviousScene")
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
else if node.state = "finished"
node.control = "stop"
@ -445,12 +516,6 @@ sub Main (args as dynamic) as void
autoPlayNextEpisode(node.id, node.showID)
end if
end if
'else if isNodeEvent(msg, "selectedExtra")
'rl = msg.getData()
'sel = rl.rowItemSelected
'? "msg.getfield():" + msg.getField()
'stop
'CreatePersonView(msg.getData())
else if type(msg) = "roDeviceInfoEvent"
event = msg.GetInfo()
group = sceneManager.callFunc("getActiveScene")

View File

@ -352,24 +352,30 @@ function CreateMovieDetailsGroup(movie)
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadPeople", movie.json)
extras.callFunc("loadParts", movie.json)
return group
end function
function CreateSeriesDetailsGroup(series)
' Get season data early in the function so we can check number of seasons.
seasonData = TVSeasons(series.id)
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1
return CreateSeasonDetailsGroupByID(series.id, seasonData.Items[0].id)
end if
group = CreateObject("roSGNode", "TVShowDetails")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = ItemMetaData(series.id)
group.seasonData = TVSeasons(series.id)
group.seasonData = seasonData ' Re-use variable from beginning of function
group.observeField("seasonSelected", m.port)
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadPeople", group.itemcontent.json)
extras.callFunc("loadParts", group.itemcontent.json)
return group
end function
@ -453,6 +459,20 @@ function CreateSeasonDetailsGroup(series, season)
return group
end function
function CreateSeasonDetailsGroupByID(seriesID, seasonID)
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.seasonData = ItemMetaData(seasonID).json
group.objects = TVEpisodes(seriesID, seasonID)
group.observeField("episodeSelected", m.port)
group.observeField("quickPlayNode", m.port)
return group
end function
function CreateItemGrid(libraryItem)
group = CreateObject("roSGNode", "ItemGrid")
group.parentItem = libraryItem
@ -461,6 +481,22 @@ function CreateItemGrid(libraryItem)
return group
end function
function CreateMovieLibraryView(libraryItem)
group = CreateObject("roSGNode", "MovieLibraryView")
group.parentItem = libraryItem
group.optionsAvailable = true
group.observeField("selectedItem", m.port)
return group
end function
function CreateMusicLibraryView(libraryItem)
group = CreateObject("roSGNode", "MusicLibraryView")
group.parentItem = libraryItem
group.optionsAvailable = true
group.observeField("selectedItem", m.port)
return group
end function
function CreateSearchPage()
' Search + Results Page
group = CreateObject("roSGNode", "searchResults")
@ -476,10 +512,10 @@ sub CreateSidePanel(buttons, options)
group.options = options
end sub
function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_idx = 1, forceTranscoding = false, showIntro = true)
function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_idx = 1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
' Video is Playing
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro)
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog)
if video = invalid then return invalid
if video.errorMsg = "introaborted" then return video
@ -490,72 +526,6 @@ function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_
return video
end function
' Play Audio
function CreateAudioPlayerGroup(audiodata)
group = CreateObject("roSGNode", "NowPlaying")
group.observeField("state", m.port)
songIDArray = CreateObject("roArray", 0, true)
' All we need is an array of Song IDs the user selected to play.
for each song in audiodata
songIDArray.push(song.id)
end for
group.pageContent = songIDArray
group.musicArtistAlbumData = audiodata
m.global.sceneManager.callFunc("pushScene", group)
return group
end function
' Play Instant Mix
function CreateInstantMixGroup(audiodata)
songList = CreateInstantMix(audiodata[0].id)
group = CreateObject("roSGNode", "NowPlaying")
group.observeField("state", m.port)
songIDArray = CreateObject("roArray", 0, true)
' All we need is an array of Song IDs the user selected to play.
for each song in songList.items
songIDArray.push(song.id)
end for
songIDArray.shift()
group.pageContent = songIDArray
group.musicArtistAlbumData = songList.items
m.global.sceneManager.callFunc("pushScene", group)
return group
end function
' Play Artist
function CreateArtistMixGroup(artistID)
songList = CreateArtistMix(artistID)
group = CreateObject("roSGNode", "NowPlaying")
group.observeField("state", m.port)
songIDArray = CreateObject("roArray", 0, true)
' All we need is an array of Song IDs the user selected to play.
for each song in songList.items
songIDArray.push(song.id)
end for
group.pageContent = songIDArray
group.musicArtistAlbumData = songList.items
m.global.sceneManager.callFunc("pushScene", group)
return group
end function
function CreatePersonView(personData as object) as object
person = CreateObject("roSGNode", "PersonDetails")
m.global.SceneManager.callFunc("pushScene", person)
@ -570,17 +540,6 @@ function CreatePersonView(personData as object) as object
return person
end function
function CreatePhotoPage(photo)
group = CreateObject("roSGNode", "PhotoDetails")
group.optionsAvailable = true
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = photo
return group
end function
sub UpdateSavedServerList()
server = get_setting("server")
username = get_setting("username")

View File

@ -1,8 +1,8 @@
function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle_idx = -1, forceTranscoding = false, showIntro = true)
function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle_idx = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
' Get video controls and UI
video = CreateObject("roSGNode", "JFVideo")
video.id = id
AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro)
AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro, allowResumeDialog)
if video.errorMsg = "introaborted"
return video
@ -19,18 +19,18 @@ function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle
return video
end function
sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1, forceTranscoding = false, showIntro = true)
sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
video.content = createObject("RoSGNode", "ContentNode")
meta = ItemMetaData(video.id)
m.videotype = meta.type
if meta = invalid
video.content = invalid
return
end if
m.videotype = meta.type
' Special handling for "Programs" or "Vidoes" launched from "On Now" or elsewhere on the home screen...
' basically anything that is a Live Channel.
if meta.json.ChannelId <> invalid
if isValid(meta?.json?.ChannelId)
if meta.json.EpisodeTitle <> invalid
meta.title = meta.json.EpisodeTitle
else if meta.json.Name <> invalid
@ -45,108 +45,115 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
end if
end if
if m.videotype = "Episode" or m.videotype = "Series"
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
video.content.contenttype = "episode"
end if
video.content.title = meta.title
video.showID = meta.showID
if playbackPosition = -1
if playbackPosition = -1 and isValid(meta.json)
playbackPosition = meta.json.UserData.PlaybackPositionTicks
if playbackPosition > 0
dialogResult = startPlayBackOver(playbackPosition)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
if dialogResult = -1
'User pressed back, return invalid and don't load video
video.content = invalid
return
else if dialogResult = 1
'Start Over selected, change position to 0
playbackPosition = 0
else if dialogResult = 2
'Mark this item as watched, refresh the page, and return invalid so we don't load the video
MarkItemWatched(video.id)
video.content.watched = not video.content.watched
group = m.scene.focusedChild
group.timeLastRefresh = CreateObject("roDateTime").AsSeconds()
group.callFunc("refresh")
video.content = invalid
return
else if dialogResult = 3
'get series ID based off episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.series_id = item.SeriesId
end for
'Get series json data
params = {
ids: m.series_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.tmp = item
end for
'Create Series Scene
CreateSeriesDetailsGroup(m.tmp)
video.content = invalid
return
if allowResumeDialog
if playbackPosition > 0
dialogResult = startPlayBackOver(playbackPosition)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
if dialogResult = -1
'User pressed back, return invalid and don't load video
video.content = invalid
return
else if dialogResult = 1
'Start Over selected, change position to 0
playbackPosition = 0
else if dialogResult = 2
'Mark this item as watched, refresh the page, and return invalid so we don't load the video
MarkItemWatched(video.id)
video.content.watched = not video.content.watched
group = m.scene.focusedChild
group.timeLastRefresh = CreateObject("roDateTime").AsSeconds()
group.callFunc("refresh")
video.content = invalid
return
else if dialogResult = 3
'get series ID based off episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.series_id = item.SeriesId
end for
'Get series json data
params = {
ids: m.series_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.tmp = item
end for
'Create Series Scene
CreateSeriesDetailsGroup(m.tmp)
video.content = invalid
return
else if dialogResult = 4
'get Season/Series ID based off episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.season_id = item.SeasonId
m.series_id = item.SeriesId
end for
'Get Series json data
params = {
ids: m.season_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.Season_tmp = item
end for
'Get Season json data
params = {
ids: m.series_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.Series_tmp = item
end for
'Create Season Scene
CreateSeasonDetailsGroup(m.Series_tmp, m.Season_tmp)
video.content = invalid
return
else if dialogResult = 4
'get Season/Series ID based off episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.season_id = item.SeasonId
m.series_id = item.SeriesId
end for
'Get Series json data
params = {
ids: m.season_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.Season_tmp = item
end for
'Get Season json data
params = {
ids: m.series_id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.Series_tmp = item
end for
'Create Season Scene
CreateSeasonDetailsGroup(m.Series_tmp, m.Season_tmp)
video.content = invalid
return
else if dialogResult = 5
'get episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.episode_id = item
end for
'Create Episode Scene
CreateMovieDetailsGroup(m.episode_id)
video.content = invalid
return
else if dialogResult = 5
'get episiode ID
params = {
ids: video.Id
}
url = Substitute("Users/{0}/Items/", get_setting("active_user"))
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
m.episode_id = item
end for
'Create Episode Scene
CreateMovieDetailsGroup(m.episode_id)
video.content = invalid
return
end if
end if
end if
end if
@ -168,7 +175,17 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
if mediaSourceId = invalid
mediaSourceId = video.id
end if
if meta.live then mediaSourceId = "" ' Don't send mediaSourceId for Live media
' Don't send mediaSourceId for Live Media
' Note: Recordings in progress will have meta.live = invalid, but we still don't want to send mediaSourceId
if not isValid(meta.live)
meta.live = false
mediaSourceId = ""
else
if meta.live
mediaSourceId = ""
end if
end if
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
video.videoId = video.id
@ -190,12 +207,22 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
video.container = getContainerType(meta)
if m.playbackInfo.MediaSources[0] = invalid
if not isValid(m.playbackInfo.MediaSources[0]) and isValid(meta.json)
m.playbackInfo = meta.json
end if
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
video.Subtitles = subtitles["all"]
if get_user_setting("playback.subs.onlytext") = "true"
safesubs = []
for each subtitle in subtitles["all"]
if subtitle["IsTextSubtitleStream"]
safesubs.push(subtitle)
end if
end for
video.Subtitles = safesubs
else
video.Subtitles = subtitles["all"]
end if
if meta.live
video.transcodeParams = {
@ -213,16 +240,20 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
fully_external = false
' For h264 video, Roku spec states that it supports and Encoding level 4.1 and 4.2.
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
' The device can decode content with a Higher Encoding level but may play it back with certain
' artifacts. If the user preference is set, and the only reason the server says we need to
' transcode is that the Envoding Level is not supported, then try to direct play but silently
' transcode is that the Encoding Level is not supported, then try to direct play but silently
' fall back to the transcode if that fails.
if meta.live = false and get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
video.directPlaySupported = true
video.transcodeAvailable = true
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
tryDirectPlay = get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
tryDirectPlay = tryDirectPlay or (get_user_setting("playback.tryDirect.hevcProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
if tryDirectPlay and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
video.directPlaySupported = true
video.transcodeAvailable = true
end if
end if
end if
@ -374,7 +405,7 @@ end function
function getContainerType(meta as object) as string
' Determine the file type of the video file source
if meta.json.mediaSources = invalid then return ""
if not IsValid(meta.json) or not isValid(meta.json.mediaSources) then return ""
container = meta.json.mediaSources[0].container
if container = invalid
@ -420,7 +451,7 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
if data <> invalid and data.Items.Count() = 2
' setup new video node
nextVideo = CreateVideoPlayerGroup(data.Items[1].Id, invalid, 1, false, false)
' remove last video scene
' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene")
if nextVideo <> invalid
m.global.sceneManager.callFunc("pushScene", nextVideo)

View File

@ -147,15 +147,19 @@ function ItemMetaData(id as string)
tmp = CreateObject("roSGNode", "MusicSongData")
' Try using song's parent for poster image
tmp.image = PosterImage(data.ParentId)
tmp.image = PosterImage(data.ParentId, { "MaxWidth": 500, "MaxHeight": 500 })
' Song's parent poster image is no good, try using the song's poster image
if tmp.image = invalid
tmp.image = PosterImage(data.id)
tmp.image = PosterImage(data.id, { "MaxWidth": 500, "MaxHeight": 500 })
end if
tmp.json = data
return tmp
else if data.type = "Recording"
' We know it's "Recording", but we don't do any special preprocessing
' for this data type at the moment, so just return the json.
return data
else
print "Items.brs::ItemMetaData processed unhandled type: " data.type
' Return json if we don't know what it is
@ -329,26 +333,32 @@ function AudioStream(id as string)
songData = AudioItem(id)
content = createObject("RoSGNode", "ContentNode")
params = {}
params.append({
"Static": "true",
"Container": songData.mediaSources[0].container
})
params.MediaSourceId = songData.mediaSources[0].id
content.url = buildURL(Substitute("Audio/{0}/stream", songData.id), params)
content.title = songData.title
content.streamformat = songData.mediaSources[0].container
playbackInfo = ItemPostPlaybackInfo(songData.id, params.MediaSourceId)
playbackInfo = ItemPostPlaybackInfo(songData.id, songData.mediaSources[0].id)
content.id = playbackInfo.PlaySessionId
if useTranscodeAudioStream(playbackInfo)
' Transcode the audio
content.url = buildURL(playbackInfo.mediaSources[0].TranscodingURL)
else
' Direct Stream the audio
params = {
"Static": "true",
"Container": songData.mediaSources[0].container,
"MediaSourceId": songData.mediaSources[0].id
}
content.streamformat = songData.mediaSources[0].container
content.url = buildURL(Substitute("Audio/{0}/stream", songData.id), params)
end if
return content
end function
function useTranscodeAudioStream(playbackInfo)
return playbackInfo.mediaSources[0] <> invalid and playbackInfo.mediaSources[0].TranscodingURL <> invalid
end function
function BackdropImage(id as string)
imgParams = { "maxHeight": "720", "maxWidth": "1280" }
return ImageURL(id, "Backdrop", imgParams)
@ -363,10 +373,6 @@ function TVSeasons(id as string)
results = []
for each item in data.Items
imgParams = { "AddPlayedIndicator": item.UserData.Played }
if item.UserData.UnplayedItemCount > 0
param = { "UnplayedCount": item.UserData.UnplayedItemCount }
imgParams.Append(param)
end if
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.image = PosterImage(item.id, imgParams)
tmp.json = item
@ -383,11 +389,7 @@ function TVEpisodes(show_id as string, season_id as string)
data = getJson(resp)
results = []
for each item in data.Items
imgParams = { "AddPlayedIndicator": item.UserData.Played, "maxWidth": 400, "maxheight": 250 }
if item.UserData.PlayedPercentage <> invalid
param = { "PercentPlayed": item.UserData.PlayedPercentage }
imgParams.Append(param)
end if
imgParams = { "maxWidth": 400, "maxheight": 250 }
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.image = PosterImage(item.id, imgParams)
if tmp.image <> invalid

Some files were not shown because too many files have changed in this diff Show More