Merge branch 'master' into 223-merge-conflicts

This commit is contained in:
Charles Ewert 2024-11-09 20:22:45 -05:00 committed by GitHub
commit ff0c444920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 2315 additions and 157 deletions

View File

@ -10,8 +10,8 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with:
node-version: "lts/*"
cache: "npm"

View File

@ -13,7 +13,7 @@ jobs:
# Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository.
contents: write
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -12,8 +12,8 @@ jobs:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'release-prep') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with:
node-version: "lts/*"
cache: "npm"

View File

@ -26,7 +26,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install required packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -50,7 +50,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ env.targetBranch }}
# Save old version again if needed
@ -101,7 +101,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install jq to update json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -125,7 +125,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ env.targetBranch }}
# Calculate new version
@ -169,7 +169,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install jq to update json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -193,7 +193,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ env.targetBranch }}
# Calculate new version

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Setup Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload artifact

View File

@ -12,8 +12,8 @@ jobs:
if: github.repository == 'jellyfin/jellyfin-roku' && github.event_name != 'pull_request' || github.repository == 'jellyfin/jellyfin-roku' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with:
node-version: "lts/*"
cache: "npm"
@ -28,7 +28,7 @@ jobs:
if: env.BRANCH_NAME == 'master'
run: npm run build-prod
- name: Setup Java
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4
with:
distribution: "temurin"
java-version: "21"

View File

@ -115,6 +115,21 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
end if
end if
if videotype = "tvchannel" and isValid(meta.json) and isValid(meta.json.CurrentProgram)
if isValid(meta.json.CurrentProgram.Name)
meta.title = `${meta.title}: ${meta.json.CurrentProgram.Name}`
end if
if isValid(meta.json.CurrentProgram.ParentIndexNumber)
video.seasonNumber = meta.json.CurrentProgram.ParentIndexNumber
end if
if isValid(meta.json.CurrentProgram.IndexNumber)
video.episodeNumber = meta.json.CurrentProgram.IndexNumber
end if
if isValid(meta.json.CurrentProgram.IndexNumberEnd)
video.episodeNumberEnd = meta.json.CurrentProgram.IndexNumberEnd
end if
end if
video.chapters = meta.json.Chapters
video.content.title = meta.title
video.showID = meta.showID

View File

@ -5,7 +5,11 @@ import "pkg:/source/utils/config.bs"
sub setFields()
json = m.top.json
m.top.id = json.id
m.top.title = json.name
if isValid(json.number)
m.top.title = `${tr("CH")} ${json.number} ${json.name}`
else
m.top.title = json.name
end if
m.top.live = true
m.top.Type = "TvChannel"
setPoster()

View File

@ -35,6 +35,7 @@ sub updateSize()
end sub
sub loadParts(data as object)
m.extrasGrp = m.top.getParent().findNode("extrasGrp")
m.top.parentId = data.id
m.people = data.People
m.LoadAdditionalPartsTask.itemId = m.top.parentId
@ -42,6 +43,7 @@ sub loadParts(data as object)
end sub
sub loadPersonVideos(personId)
m.extrasGrp = m.top.getParent().findNode("extrasGrp")
m.personId = personId
m.LoadMoviesTask.itemId = m.personId
m.LoadMoviesTask.observeField("content", "onMoviesLoaded")
@ -114,7 +116,7 @@ sub onLikeThisLoaded()
m.SpecialFeaturesTask.control = "RUN"
end sub
function onSpecialFeaturesLoaded()
sub onSpecialFeaturesLoaded()
data = m.SpecialFeaturesTask.content
m.SpecialFeaturesTask.unobserveField("content")
if data <> invalid and data.count() > 0
@ -132,8 +134,8 @@ function onSpecialFeaturesLoaded()
addRowSize([462, 372])
end if
return m.top.content
end function
showOrHideMe()
end sub
sub onMoviesLoaded()
data = m.LoadMoviesTask.content
@ -179,6 +181,8 @@ sub onSeriesLoaded()
m.top.content.appendChild(row)
end if
m.top.visible = true
showOrHideMe()
end sub
function buildRow(rowTitle as string, items, imgWdth = 0)
@ -218,6 +222,17 @@ sub addRowSize(newRow)
m.top.rowItemSize = newSizeArray
end sub
' don't show popup panel if there is nothing to show
sub showOrHideMe()
if isValid(m.top.content)
if m.top.content.getChildCount() = 0
m.extrasGrp.visible = false
else
m.extrasGrp.visible = true
end if
end if
end sub
sub onRowItemSelected()
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
end sub

View File

@ -51,20 +51,30 @@ sub loadLibraries()
end sub
sub updateSize()
m.top.translation = [111, 180]
itemHeight = 330
uiRowLayout = m.global.session.user.settings["ui.row.layout"]
'Set width of Rows to cut off at edge of Safe Zone
m.top.itemSize = [1703, itemHeight]
if isValid(uiRowLayout)
if uiRowLayout = "fullwidth"
m.top.translation = [0, 180]
' rows take up full width of the screen
m.top.itemSize = [1920, 330]
' align with edge of "action" safe zone
m.top.focusXOffset = [96]
m.top.rowLabelOffset = [96, 20]
else
' original layout
m.top.translation = [111, 180]
m.top.itemSize = [1703, 330]
' reset to defaults
m.top.focusXOffset = []
m.top.rowLabelOffset = [0, 20]
end if
end if
' spacing between rows
m.top.itemSpacing = [0, 105]
' spacing between items in a row
m.top.rowItemSpacing = [20, 0]
' Default size to wide poster, the most used size
m.top.rowItemSize = homeRowItemSizes.WIDE_POSTER
m.top.rowItemSpacing = [21, 0]
m.top.visible = true
end sub
@ -455,6 +465,8 @@ end sub
sub updateHomeRows()
' Hide the row counter to prevent flicker. We'll show it once loading timer fires
m.top.showRowCounter = [false]
m.top.visible = false
updateSize()
processUserSections()
end sub
@ -687,6 +699,7 @@ end sub
sub itemSelected()
m.selectedRowItem = m.top.rowItemSelected
m.global.launchSource = "home"
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
'Prevent the selected item event from double firing

View File

@ -403,7 +403,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.options.setFocus(true)
end if
if key = "down" and m.buttonGrp.isInFocusChain()
if key = "down" and m.buttonGrp.isInFocusChain() and m.extrasGrp.visible = true
m.top.lastFocus = m.extrasGrid
m.extrasGrid.setFocus(true)
m.top.findNode("VertSlider").reverse = false

View File

@ -6,11 +6,28 @@ sub init()
m.top.showRowLabel = [false]
m.top.observeField("selectItemId", "onItemSelected")
updateSize()
m.top.setFocus(true)
end sub
sub onItemSelected()
Id = m.top.selectItemId
if Id <> invalid and Id <> "" and m.top.objects <> invalid
for i = 0 to m.top.objects.items.count() - 1
item = m.top.objects.items[i]
if item.id = Id
m.top.jumpToItem = i
return
end if
end for
end if
end sub
sub updateSize()
m.top.translation = [450, 180]

View File

@ -4,5 +4,8 @@
<field id="objects" type="assocarray" onChange="setupRows" />
<field id="escapeButton" type="string" alwaysNotify="true" />
<field id="doneLoading" type="boolean" />
<field id="selectItemId" type="string" />
<function name="onItemSelected" />
</interface>
</component>

View File

@ -4,9 +4,15 @@ sub init()
m.rows = m.top.findNode("tvEpisodeRow")
m.tvListOptions = m.top.findNode("tvListOptions")
m.top.observeField("selectItemId", "onItemSelected")
m.rows.observeField("doneLoading", "rowsDoneLoading")
end sub
sub onItemSelected()
Id = m.top.selectItemId
m.rows.selectItemId = Id
end sub
sub setupRows()
objects = m.top.objects
m.rows.objects = objects

View File

@ -8,5 +8,8 @@
<field id="objects" type="assocarray" onChange="setupRows" />
<field id="itemSelected" type="integer" />
<field id="doneLoading" type="boolean" />
<field id="selectItemId" type="string" />
<function name="onItemSelected" />
</interface>
</component>

View File

@ -6,6 +6,7 @@ import "pkg:/source/api/sdk.bs"
sub init()
m.top.optionsAvailable = false
m.top.observeField("selectItemId", "onItemSelected")
m.rows = m.top.findNode("picker")
m.poster = m.top.findNode("seasonPoster")
@ -25,6 +26,11 @@ sub setSeasonLoading()
m.top.overhangTitle = tr("Loading...")
end sub
sub onItemSelected()
Id = m.top.selectItemId
m.rows.selectItemId = Id
end sub
' Updates the visibility of the Extras button based on if this season has any extra features
sub setExtraButtonVisibility()
if isValid(m.top.extrasObjects) and isValidAndNotEmpty(m.top.extrasObjects.items)
@ -73,6 +79,7 @@ end function
' OnScreenShown: Callback function when view is presented on screen
'
sub OnScreenShown()
if m.isFirstRun
m.isFirstRun = false
return
@ -80,6 +87,7 @@ sub OnScreenShown()
m.tvEpisodeRow.setFocus(true)
m.top.refreshSeasonDetailsData = not m.top.refreshSeasonDetailsData
end sub
' Handle navigation input from the remote and act on it

View File

@ -19,6 +19,9 @@
<field id="objects" alias="picker.objects" />
<field id="episodeObjects" type="assocarray" />
<field id="extrasObjects" type="assocarray" onChange="setExtraButtonVisibility" />
<field id="selectItemId" type="string" />
<function name="updateSeason" />
<function name="onItemSelected" />
</interface>
</component>

View File

@ -41,7 +41,21 @@ end sub
'
sub onProgressPercentageChanged()
m.videoPositionTime.text = secondsToHuman(m.top.positionTime, true)
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true)
osdmode = m.global.session.user.settings["ui.general.osdremainingtime"]
if m.global.session.user.settings["ui.design.hideclock"]
' in order to honor the hide clocks setting
osdmode = "remaining"
end if
if osdmode = "remaining"
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true)
else if osdmode = "timeofday"
m.videoRemainingTime.text = secondsToEndTime(m.top.remainingPositionTime)
else if osdmode = "both"
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true) + " | " + secondsToEndTime(m.top.remainingPositionTime)
end if
m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage
end sub

View File

@ -34,7 +34,7 @@
</Rectangle>
<Label id="videoPositionTime" font="font:MediumSystemFont" color="0xffffffFF" translation="[103,985]" />
<Label id="videoRemainingTime" font="font:MediumSystemFont" color="0xffffffFF" horizAlign="right" width="200" translation="[1617,985]" />
<Label id="videoRemainingTime" font="font:MediumSystemFont" color="0xffffffFF" horizAlign="right" width="400" translation="[1450,985]" />
<Timer id="inactivityTimer" duration="1" repeat="true" />
</children>

View File

@ -412,7 +412,7 @@ sub onVideoContentLoaded()
m.osd.itemTitleText = m.top.content.title
' If video is an episode, attempt to add season and episode numbers to OSD
if m.top.content.contenttype = 4
if m.top.content.contenttype = 4 or m.top.content.live
if isValid(videoContent[0].seasonNumber)
m.osd.seasonNumber = videoContent[0].seasonNumber
end if

View File

@ -304,7 +304,7 @@ sub setTvShowsOptions(options)
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("DATE_PLAYED"), "Name": "SeriesDatePlayed" },
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("Random"), "Name": "Random" },

View File

@ -117,6 +117,21 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
end if
end if
if videotype = "tvchannel" and isValid(meta.json) and isValid(meta.json.CurrentProgram)
if isValid(meta.json.CurrentProgram.Name)
meta.title = `${meta.title}: ${meta.json.CurrentProgram.Name}`
end if
if isValid(meta.json.CurrentProgram.ParentIndexNumber)
video.seasonNumber = meta.json.CurrentProgram.ParentIndexNumber
end if
if isValid(meta.json.CurrentProgram.IndexNumber)
video.episodeNumber = meta.json.CurrentProgram.IndexNumber
end if
if isValid(meta.json.CurrentProgram.IndexNumberEnd)
video.episodeNumberEnd = meta.json.CurrentProgram.IndexNumberEnd
end if
end if
video.chapters = meta.json.Chapters
video.content.title = meta.title
video.showID = meta.showID

View File

@ -7,7 +7,11 @@ import "pkg:/source/utils/config.bs"
sub setFields()
json = m.top.json
m.top.id = json.id
m.top.title = json.name
if isValid(json.number)
m.top.title = `${tr("CH")} ${json.number} ${json.name}`
else
m.top.title = json.name
end if
m.top.live = true
m.top.Type = "TvChannel"
setPoster()

View File

@ -37,6 +37,7 @@ sub updateSize()
end sub
sub loadParts(data as object)
m.extrasGrp = m.top.getParent().findNode("extrasGrp")
m.top.parentId = data.id
m.people = data.People
m.LoadAdditionalPartsTask.itemId = m.top.parentId
@ -44,6 +45,7 @@ sub loadParts(data as object)
end sub
sub loadPersonVideos(personId)
m.extrasGrp = m.top.getParent().findNode("extrasGrp")
m.personId = personId
m.LoadMoviesTask.itemId = m.personId
m.LoadMoviesTask.observeField("content", "onMoviesLoaded")
@ -116,7 +118,7 @@ sub onLikeThisLoaded()
m.SpecialFeaturesTask.control = "RUN"
end sub
function onSpecialFeaturesLoaded()
sub onSpecialFeaturesLoaded()
data = m.SpecialFeaturesTask.content
m.SpecialFeaturesTask.unobserveField("content")
if data &lt;> invalid and data.count() > 0
@ -134,8 +136,8 @@ function onSpecialFeaturesLoaded()
addRowSize([462, 372])
end if
return m.top.content
end function
showOrHideMe()
end sub
sub onMoviesLoaded()
data = m.LoadMoviesTask.content
@ -181,6 +183,8 @@ sub onSeriesLoaded()
m.top.content.appendChild(row)
end if
m.top.visible = true
showOrHideMe()
end sub
function buildRow(rowTitle as string, items, imgWdth = 0)
@ -220,6 +224,17 @@ sub addRowSize(newRow)
m.top.rowItemSize = newSizeArray
end sub
' don't show popup panel if there is nothing to show
sub showOrHideMe()
if isValid(m.top.content)
if m.top.content.getChildCount() = 0
m.extrasGrp.visible = false
else
m.extrasGrp.visible = true
end if
end if
end sub
sub onRowItemSelected()
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
end sub

View File

@ -53,20 +53,30 @@ sub loadLibraries()
end sub
sub updateSize()
m.top.translation = [111, 180]
itemHeight = 330
uiRowLayout = m.global.session.user.settings["ui.row.layout"]
'Set width of Rows to cut off at edge of Safe Zone
m.top.itemSize = [1703, itemHeight]
if isValid(uiRowLayout)
if uiRowLayout = "fullwidth"
m.top.translation = [0, 180]
' rows take up full width of the screen
m.top.itemSize = [1920, 330]
' align with edge of "action" safe zone
m.top.focusXOffset = [96]
m.top.rowLabelOffset = [96, 20]
else
' original layout
m.top.translation = [111, 180]
m.top.itemSize = [1703, 330]
' reset to defaults
m.top.focusXOffset = []
m.top.rowLabelOffset = [0, 20]
end if
end if
' spacing between rows
m.top.itemSpacing = [0, 105]
' spacing between items in a row
m.top.rowItemSpacing = [20, 0]
' Default size to wide poster, the most used size
m.top.rowItemSize = homeRowItemSizes.WIDE_POSTER
m.top.rowItemSpacing = [21, 0]
m.top.visible = true
end sub
@ -457,6 +467,8 @@ end sub
sub updateHomeRows()
' Hide the row counter to prevent flicker. We'll show it once loading timer fires
m.top.showRowCounter = [false]
m.top.visible = false
updateSize()
processUserSections()
end sub
@ -689,6 +701,7 @@ end sub
sub itemSelected()
m.selectedRowItem = m.top.rowItemSelected
m.global.launchSource = "home"
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
'Prevent the selected item event from double firing

View File

@ -75,6 +75,7 @@ sub loadItems()
params["DisableFirstEpisode"] = false
params["limit"] = 24
params["EnableTotalRecordCount"] = false
params["EnableResumable"] = false
maxDaysInNextUp = userSettings["ui.details.maxdaysnextup"].ToInt()
if isValid(maxDaysInNextUp)

View File

@ -405,7 +405,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.options.setFocus(true)
end if
if key = "down" and m.buttonGrp.isInFocusChain()
if key = "down" and m.buttonGrp.isInFocusChain() and m.extrasGrp.visible = true
m.top.lastFocus = m.extrasGrid
m.extrasGrid.setFocus(true)
m.top.findNode("VertSlider").reverse = false

View File

@ -8,11 +8,28 @@
m.top.showRowLabel = [false]
m.top.observeField("selectItemId", "onItemSelected")
updateSize()
m.top.setFocus(true)
end sub
sub onItemSelected()
Id = m.top.selectItemId
if Id &lt;> invalid and Id &lt;> "" and m.top.objects &lt;> invalid
for i = 0 to m.top.objects.items.count() - 1
item = m.top.objects.items[i]
if item.id = Id
m.top.jumpToItem = i
return
end if
end for
end if
end sub
sub updateSize()
m.top.translation = [450, 180]

View File

@ -6,9 +6,15 @@ sub init()
m.rows = m.top.findNode("tvEpisodeRow")
m.tvListOptions = m.top.findNode("tvListOptions")
m.top.observeField("selectItemId", "onItemSelected")
m.rows.observeField("doneLoading", "rowsDoneLoading")
end sub
sub onItemSelected()
Id = m.top.selectItemId
m.rows.selectItemId = Id
end sub
sub setupRows()
objects = m.top.objects
m.rows.objects = objects

View File

@ -8,6 +8,7 @@ import "pkg:/source/api/sdk.bs"
sub init()
m.top.optionsAvailable = false
m.top.observeField("selectItemId", "onItemSelected")
m.rows = m.top.findNode("picker")
m.poster = m.top.findNode("seasonPoster")
@ -27,6 +28,11 @@ sub setSeasonLoading()
m.top.overhangTitle = tr("Loading...")
end sub
sub onItemSelected()
Id = m.top.selectItemId
m.rows.selectItemId = Id
end sub
' Updates the visibility of the Extras button based on if this season has any extra features
sub setExtraButtonVisibility()
if isValid(m.top.extrasObjects) and isValidAndNotEmpty(m.top.extrasObjects.items)
@ -75,6 +81,7 @@ end function
' OnScreenShown: Callback function when view is presented on screen
'
sub OnScreenShown()
if m.isFirstRun
m.isFirstRun = false
return
@ -82,6 +89,7 @@ sub OnScreenShown()
m.tvEpisodeRow.setFocus(true)
m.top.refreshSeasonDetailsData = not m.top.refreshSeasonDetailsData
end sub
' Handle navigation input from the remote and act on it

View File

@ -43,7 +43,21 @@ end sub
'
sub onProgressPercentageChanged()
m.videoPositionTime.text = secondsToHuman(m.top.positionTime, true)
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true)
osdmode = m.global.session.user.settings["ui.general.osdremainingtime"]
if m.global.session.user.settings["ui.design.hideclock"]
' in order to honor the hide clocks setting
osdmode = "remaining"
end if
if osdmode = "remaining"
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true)
else if osdmode = "timeofday"
m.videoRemainingTime.text = secondsToEndTime(m.top.remainingPositionTime)
else if osdmode = "both"
m.videoRemainingTime.text = secondsToHuman(m.top.remainingPositionTime, true) + " | " + secondsToEndTime(m.top.remainingPositionTime)
end if
m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage
end sub

View File

@ -11,6 +11,7 @@ sub init()
m.top.getScene().findNode("overhang").visible = false
userSettings = m.global.session.user.settings
m.currentItem = m.global.queueManager.callFunc("getCurrentItem")
m.originalClosedCaptionState = invalid
m.top.id = m.currentItem.id
m.top.seekMode = "accurate"
@ -413,7 +414,7 @@ sub onVideoContentLoaded()
m.osd.itemTitleText = m.top.content.title
' If video is an episode, attempt to add season and episode numbers to OSD
if m.top.content.contenttype = 4
if m.top.content.contenttype = 4 or m.top.content.live
if isValid(videoContent[0].seasonNumber)
m.osd.seasonNumber = videoContent[0].seasonNumber
end if
@ -463,7 +464,12 @@ sub onVideoContentLoaded()
availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName)
if availableSubtitleTrackIndex &lt;> -1
if not selectedSubtitle.IsEncoded
m.top.globalCaptionMode = "On"
if selectedSubtitle.IsForced
' If IsForced, make sure to remember the Roku global setting so we
' can set it back when the video is done playing.
m.originalClosedCaptionState = m.top.globalCaptionMode
m.top.globalCaptionMode = "On"
end if
m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
end if
end if
@ -699,6 +705,12 @@ sub ReportPlayback(state = "update" as string)
m.bufferCheckTimer.duration = 30
end if
if (state = "stop" or state = "finished") and m.originalClosedCaptionState &lt;> invalid
m.log.debug("ReportPlayback() setting", m.top.globalCaptionMode, "back to", m.originalClosedCaptionState)
m.top.globalCaptionMode = m.originalClosedCaptionState
m.originalClosedCaptionState = invalid
end if
' Report playstate via worker task
playstateTask = m.global.playstateTask
playstateTask.setFields({ status: state, params: params })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,7 @@
sceneManager = CreateObject("roSGNode", "SceneManager")
sceneManager.observeField("dataReturned", m.port)
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager, launchSource: "" })
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") })
@ -309,22 +309,39 @@
audio_stream_idx = selectedItem.selectedAudioStreamIndex
end if
launchSource = m.global.launchSource
m.global.launchSource = ""
selectedItem.selectedAudioStreamIndex = audio_stream_idx
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
localGlobal = m.global
' we only use this special steering logic from the home screen
if launchSource &lt;> "home"
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
end if
else if localGlobal.session.user.settings["ui.general.episodenextupbehavior"] = "episode"
group = CreateMovieDetailsGroup(selectedItem)
else if localGlobal.session.user.settings["ui.general.episodenextupbehavior"] = "season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.json.SeasonId, selectedItem.id)
end if
else if selectedItemType = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json.id)
else if selectedItemType = "Season"
if isValid(selectedItem.json) and isValid(selectedItem.json.SeriesId) and isValid(selectedItem.id)
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id, "")
else
stopLoadingSpinner()
message_dialog(tr("Error loading Season"))
@ -865,7 +882,7 @@
else if popupNode.returnData.indexselected = 3
' User chose Go to season
if isValid(selectedItem[0].json) and isValid(selectedItem[0].json.SeriesId) and isValid(selectedItem[0].json.seasonID)
CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID)
CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID, "")
else
stopLoadingSpinner()
message_dialog(tr("Error loading Season"))

View File

@ -648,7 +648,7 @@ function CreateSeriesDetailsGroup(seriesID as string) as dynamic
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
if seasonData &lt;> invalid and m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] and seasonData.Items.Count() = 1
stopLoadingSpinner()
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id, "")
end if
' start building SeriesDetails view
group = CreateObject("roSGNode", "TVShowDetails")
@ -795,7 +795,7 @@ function CreateSeasonDetailsGroup(series as object, season as object) as dynamic
return group
end function
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as dynamic
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string, selectId as string) as dynamic
' validate parameters
if seriesID = "" or seasonID = "" then return invalid
@ -807,9 +807,11 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as
stopLoadingSpinner()
return invalid
end if
' start building SeasonDetails view
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
' push scene asap (to prevent extra button presses when retriving series/movie info)
group.seasonData = seasonMetaData.json
group.objects = TVEpisodes(seriesID, seasonID)
@ -823,6 +825,10 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as
' check for specials/extras for this season
group.extrasObjects = TVSeasonExtras(seasonID)
group.observeField("sceneReady", "onSceneReady")
group.selectItemId = selectId
' finished building SeasonDetails view
return group
end function

View File

@ -49,8 +49,8 @@ function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioT
' force the server to transcode AAC profiles we don't support to MP3 instead of the usual AAC
' TODO: Remove this after server adds support for transcoding AAC from one profile to another
if LCase(selectedAudioStream.Codec) = "aac"
if LCase(selectedAudioStream.Profile) = "main" or LCase(selectedAudioStream.Profile) = "he-aac"
if selectedAudioStream.Codec &lt;> invalid and LCase(selectedAudioStream.Codec) = "aac"
if selectedAudioStream.Profile &lt;> invalid and LCase(selectedAudioStream.Profile) = "main" or LCase(selectedAudioStream.Profile) = "he-aac"
for each rule in deviceProfile.TranscodingProfiles
if rule.Container = "ts" or rule.Container = "mp4"
if rule.AudioCodec = "aac"

View File

@ -303,21 +303,11 @@ function getTranscodingProfiles() as object
end if
' AV1
for each container in transcodingContainers
if di.CanDecodeVideo({ Codec: "av1", Container: container }).Result
if container = "mp4"
' check for codec string before adding it
if mp4VideoCodecs.Instr(0, ",av1") = -1
mp4VideoCodecs = mp4VideoCodecs + ",av1"
end if
else if container = "ts"
' check for codec string before adding it
if tsVideoCodecs.Instr(0, ",av1") = -1
tsVideoCodecs = tsVideoCodecs + ",av1"
end if
end if
end if
end for
' CanDecodeVideo() returns false for AV1 when the container is provided
' Manually add AV1 to the mp4VideoCodecs list if support is detected
if di.CanDecodeVideo({ Codec: "av1" }).Result
mp4VideoCodecs = mp4VideoCodecs + ",av1"
end if
' AUDIO CODECS
for each container in transcodingContainers

View File

@ -72,6 +72,24 @@ function secondsToHuman(totalSeconds as integer, addLeadingMinuteZero as boolean
return humanTime
end function
function secondsToEndTime(totalSeconds) as string
' Get the current time in seconds since midnight UTC (Unix Epoch Time)
currentUTCTime = CreateObject("roDateTime").AsSeconds()
' Calculate the target time in seconds by adding the number of seconds
targetTimeInSeconds = currentUTCTime + totalSeconds
' Create a new roDateTime object for the target time
targetDateTime = CreateObject("roDateTime")
targetDateTime.FromSeconds(targetTimeInSeconds)
targetDateTime.ToLocalTime()
formattedTime = formatTime(targetDateTime)
return formattedTime
end function
' Format time as 12 or 24 hour format based on system clock setting
function formatTime(time) as string
hours = time.getHours()

View File

@ -1338,6 +1338,54 @@
<translation>Serienbild verwenden</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Play Next Episode Automatically</source>
<translation>Nächste Episode automatisch abspielen</translation>
</message>
<message>
<source>When finished playing a single episode, play the next one automatically.</source>
<translation>Wenn die Wiedergabe einer einzelnen Episode beendet ist, wird automatisch die nächste Episode abgespielt.</translation>
</message>
<message>
<source>Special</source>
<translation>Special</translation>
</message>
<message>
<source>Episode Next Up Behavior</source>
<translation>Nächste Episode Verhalten</translation>
</message>
<message>
<source>This controls what clicking OK on a Next Up episode does.</source>
<translation>Damit wird gesteuert, was ein Klick auf OK bei einer Folge von Next Up bewirkt.</translation>
</message>
<message>
<source>View Episode Details</source>
<translation>Details zur Episode ansehen</translation>
</message>
<message>
<source>View Season Details</source>
<translation>Staffel Details ansehen</translation>
</message>
<message>
<source>OSD Remaining Time</source>
<translation>verbleibende OSD Zeit</translation>
</message>
<message>
<source>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</source>
<translation>Wie wird die verbleibende Zeit dargestellt? Die verbleibende Zeit zeigt die verbleibenden Stunden, Minuten und Sekunden an. Die Tageszeit zeigt an, wann das Video beendet sein wird.</translation>
</message>
<message>
<source>Remaining Time</source>
<translation>verbleibende Zeit</translation>
</message>
<message>
<source>Time of Day</source>
<translation>Tageszeit</translation>
</message>
<message>
<source>Both</source>
<translation>Beide</translation>
</message>
</context>
<context>
<name></name>

View File

@ -1322,6 +1322,46 @@
<source>When finished playing a single episode, play the next one automatically.</source>
<translation>When finished playing a single episode, play the next one automatically.</translation>
</message>
<message>
<source>Special</source>
<translation>Special</translation>
</message>
<message>
<source>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</source>
<translation>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</translation>
</message>
<message>
<source>Episode Next Up Behavior</source>
<translation>Episode Next Up Behaviour</translation>
</message>
<message>
<source>This controls what clicking OK on a Next Up episode does.</source>
<translation>This controls what clicking OK on a Next Up episode does.</translation>
</message>
<message>
<source>View Episode Details</source>
<translation>View Episode Details</translation>
</message>
<message>
<source>View Season Details</source>
<translation>View Season Details</translation>
</message>
<message>
<source>OSD Remaining Time</source>
<translation>OSD Remaining Time</translation>
</message>
<message>
<source>Remaining Time</source>
<translation>Remaining Time</translation>
</message>
<message>
<source>Time of Day</source>
<translation>Time of Day</translation>
</message>
<message>
<source>Both</source>
<translation>Both</translation>
</message>
</context>
<context>
<name></name>

View File

@ -1321,6 +1321,71 @@
<translation>Use Show Image</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Row Layout</source>
<translation>Row Layout</translation>
<extracomment>User Setting - Setting title</extracomment>
</message>
<message>
<source>Choose how rows are displayed on the home screen.</source>
<translation>Choose how rows are displayed on the home screen.</translation>
<extracomment>User Setting - Setting description</extracomment>
</message>
<message>
<source>Original</source>
<translation>Original</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Full Width</source>
<translation>Full Width</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Episode Next Up Behavior</source>
<translation>Episode Next Up Behavior</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>This controls what clicking OK on a Next Up episode does.</source>
<translation>This controls what clicking OK on a Next Up episode does.</translation>
<extracomment>User Setting - Setting description</extracomment>
</message>
<message>
<source>View Episode Details</source>
<translation>View Episode Details</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>View Season Details</source>
<translation>View Season Details</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>OSD Remaining Time</source>
<translation>OSD Remaining Time</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</source>
<translation>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</translation>
<extracomment>User Setting - Setting description</extracomment>
</message>
<message>
<source>Remaining Time</source>
<translation>Remaining Time</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Time of Day</source>
<translation>Time of Day</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Both</source>
<translation>Both</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Special</source>
<translation>Special</translation>
@ -1331,5 +1396,10 @@
<translation>N/A</translation>
<extracomment>Abbreviation for not available</extracomment>
</message>
<message>
<source>CH</source>
<translation>CH</translation>
<extracomment>Abbreviation for Channel</extracomment>
</message>
</context>
</TS>

View File

@ -1330,6 +1330,54 @@
<translation>Images d&apos;épisodes à suivre</translation>
<extracomment>User Setting - Setting title</extracomment>
</message>
<message>
<source>Play Next Episode Automatically</source>
<translation>Lancer automatiquement le nouvel épisode</translation>
</message>
<message>
<source>Special</source>
<translation>Spécial</translation>
</message>
<message>
<source>When finished playing a single episode, play the next one automatically.</source>
<translation>Lorsque la lecture d&apos;un épisode est terminée, lancer automatiquement le suivant.</translation>
</message>
<message>
<source>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</source>
<translation>Comment le temps restant est-il représenté ? Le temps restant indique les heures, les minutes et les secondes restantes. L&apos;heure du jour indique l&apos;heure à laquelle la vidéo se terminera.</translation>
</message>
<message>
<source>Episode Next Up Behavior</source>
<translation type="unfinished">Comportement du prochain épisode</translation>
</message>
<message>
<source>This controls what clicking OK on a Next Up episode does.</source>
<translation>Cela contrôle ce que fait un clic sur OK sur un épisode suivant.</translation>
</message>
<message>
<source>View Episode Details</source>
<translation>Voir les détails de l&apos;épisode</translation>
</message>
<message>
<source>View Season Details</source>
<translation>Voir les détails de la saison</translation>
</message>
<message>
<source>OSD Remaining Time</source>
<translation type="unfinished">Affichage à l&apos;écran du temps restant</translation>
</message>
<message>
<source>Remaining Time</source>
<translation>Temps restant</translation>
</message>
<message>
<source>Time of Day</source>
<translation>Heure de la journée</translation>
</message>
<message>
<source>Both</source>
<translation>Les deux</translation>
</message>
</context>
<context>
<name></name>

View File

@ -1353,6 +1353,54 @@
<translation>Usar imagem da série</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Play Next Episode Automatically</source>
<translation>Reproduzir o próximo episódio automaticamente</translation>
</message>
<message>
<source>When finished playing a single episode, play the next one automatically.</source>
<translation>Quando terminar de reproduzir um único episodio, reproduz o próximo de forma automática.</translation>
</message>
<message>
<source>Special</source>
<translation>Especial</translation>
</message>
<message>
<source>How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.</source>
<translation>Como o tempo restante é representado. O tempo exibe as horas, minutos e segundos restantes. A hora do dia mostra o horário em que o vídeo será concluído.</translation>
</message>
<message>
<source>Episode Next Up Behavior</source>
<translation>Comportamento do A Seguir</translation>
</message>
<message>
<source>View Episode Details</source>
<translation>Ver Detalhes do Episódio</translation>
</message>
<message>
<source>This controls what clicking OK on a Next Up episode does.</source>
<translation>Isso controla o comportamento ao clicar OK no A Seguir.</translation>
</message>
<message>
<source>View Season Details</source>
<translation>Ver Detalhes da Temporada</translation>
</message>
<message>
<source>Remaining Time</source>
<translation>Tempo Restante</translation>
</message>
<message>
<source>OSD Remaining Time</source>
<translation>Tempo Restante de Exibição na Tela</translation>
</message>
<message>
<source>Time of Day</source>
<translation>Hora do Dia</translation>
</message>
<message>
<source>Both</source>
<translation>Ambos</translation>
</message>
</context>
<context>
<name></name>

View File

@ -581,6 +581,310 @@
<translation>Necunoscut</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Adu experiența cinematografică direct în sufrageria ta, cu posibilitatea de a reda introduceri personalizate înainte de filmul principal.</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Slideshow Paused</translation>
</message>
<message>
<source>Random Off</source>
<translation>Aleatoriu Oprit</translation>
</message>
<message>
<source>Use Splashscreen as Home Background</source>
<translation>Folosește Ecranul de Start ca Fundal pentru Pagina Principală</translation>
</message>
<message>
<source>Audio Codec</source>
<translation>Codec Audio</translation>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>Poți căuta Titluri, Persoane, Canale TV live și altele</translation>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Încearcă redarea directă pentru media HEVC cu niveluri de profil nesuportate, înainte de a recurge la transcodare dacă aceasta eșuează.</translation>
</message>
<message>
<source>Reason</source>
<translation>Motiv</translation>
</message>
<message>
<source>Video Codec</source>
<translation>Codec Video</translation>
</message>
<message>
<source>Size</source>
<translation>Dimensiune</translation>
</message>
<message>
<source>Use voice remote to search</source>
<translation>Folosește căutare vocală</translation>
</message>
<message>
<source>H.264</source>
<translation>H.264</translation>
</message>
<message>
<source>Settings relating to how the application looks.</source>
<translation>Setări referitoare la aspectul aplicației.</translation>
</message>
<message>
<source>Go directly to the episode list if a TV series has only one season.</source>
<translation>Mergi direct la lista de episoade dacă un serial are un singur sezon.</translation>
</message>
<message>
<source>Blur images of unwatched episodes.</source>
<translation>Estompează imaginile episoadelor nevizionate.</translation>
</message>
<message>
<source>Hide Clock</source>
<translation>Ascunde Ceasul</translation>
</message>
<message>
<source>Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.</source>
<translation>Ascunde ceasul peste tot in Jellyfin. Va fi necesar închizi și redeschizi Jellyfin pentru ca modificările aibă efect.</translation>
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Încearcă redarea directă pentru media H.264 cu niveluri de profil nesuportate, înainte de a recurge la transcodare dacă aceasta eșuează.</translation>
</message>
<message>
<source>Custom Subtitles</source>
<translation>Subtitrări Personalizate</translation>
</message>
<message>
<source>Replace Roku&apos;s default subtitle functions with custom functions that support CJK fonts. Fallback fonts must be configured and enabled on the server for CJK rendering to work.</source>
<translation>Înlocuiește funcțiile implicite de subtitrare ale Roku cu funcții personalizate care suportă fonturi CJK (chineză, japoneză, coreeană). Fonturile de rezervă trebuie configurate și activate pe server pentru ca redarea CJK funcționeze.</translation>
</message>
<message>
<source>all</source>
<translation>toate</translation>
</message>
<message>
<source>Slideshow Off</source>
<translation>Slideshow Off</translation>
</message>
<message>
<source>Slideshow On</source>
<translation>Slideshow On</translation>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>Suport MPEG-4</translation>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Slideshow Resumed</translation>
</message>
<message>
<source>Parental Ratings</source>
<translation>Evaluări Parentale</translation>
</message>
<message>
<source>Years</source>
<translation>Ani</translation>
</message>
<message>
<source>Show What&apos;s New Popup</source>
<translation>Afișează Popup-ul Ce e Nou</translation>
</message>
<message>
<source>Show What&apos;s New popup when Jellyfin is updated to a new version.</source>
<translation>Afișează popup-ul Ce e Nou când Jellyfin este actualizat la o versiune nouă.</translation>
</message>
<message>
<source>Next episode</source>
<translation>Următorul episod</translation>
</message>
<message>
<source>Random On</source>
<translation>Aleatoriu Pornit</translation>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Filme (Prezentare)</translation>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Filme (Grilă)</translation>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Afișează numărul de elemente din bibliotecă și indexul elementului selectat.</translation>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Iată codul tău de Conectare Rapidă:</translation>
</message>
<message>
<source>Hide Taglines</source>
<translation>Ascunde sloganurile</translation>
</message>
<message>
<source>Design Elements</source>
<translation>Elemente de Design</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Redă Trailer</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Informații de transcodare</translation>
</message>
<message>
<source>Audio Channels</source>
<translation>Canale Audio</translation>
</message>
<message>
<source>Level</source>
<translation>Nivel</translation>
</message>
<message>
<source>Video range type</source>
<translation>Tipul intervalului video</translation>
</message>
<message>
<source>Set Favorite</source>
<translation>Setează favorit</translation>
</message>
<message>
<source>Set Watched</source>
<translation>Marcheaza ca vizionat</translation>
</message>
<message>
<source>Go to series</source>
<translation>Mergi la sezoane</translation>
</message>
<message>
<source>Search now</source>
<translation>Caută acum</translation>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(Dialogul se va închide automat)</translation>
</message>
<message>
<source>Return to Top</source>
<translation>Revino la început</translation>
</message>
<message>
<source>Studios</source>
<translation>Studiouri</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>Folosește butonul de reluare pentru a anima încet până la primul element din folder. (Dacă este dezactivat, folderul va reveni imediat la primul element).</translation>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Ascunde textul de slogan pe paginile de detalii.</translation>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Opțiuni pentru seriale TV.</translation>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Estompează episoadele nevizionate</translation>
</message>
<message>
<source>Skip Details for Single Seasons</source>
<translation>Omite detaliile pentru sezoanele individuale</translation>
</message>
<message>
<source>Options that alter the design of Jellyfin.</source>
<translation>Opțiuni ce modifică designul Jellyfin.</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>Folosește imaginea generată a ecranului de start ca fundal pentru pagina principală a Jellyfin. Va fi necesar închizi și redeschizi Jellyfin pentru ca schimbarea aibă efect.</translation>
</message>
<message>
<source>Cinema Mode</source>
<translation>Mod Cinema</translation>
</message>
<message>
<source>HEVC</source>
<translation>HEVC</translation>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Setări referitoare la redare și la codec-urile și tipurile de media acceptate.</translation>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Zile Maxime la Următorul Episod</translation>
</message>
<message>
<source>Playback Information</source>
<translation>Informații de redare</translation>
</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>Setează numărul maxim de zile în care un serial ar trebui rămână în lista Următorul Episod fără a fi vizionat.</translation>
</message>
<message>
<source>direct</source>
<translation>direct</translation>
</message>
<message>
<source>Total Bitrate</source>
<translation>Bitrate Total</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Informații Stream</translation>
</message>
<message>
<source>Codec</source>
<translation>Codec</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Tag Codec</translation>
</message>
<message>
<source>Bit Rate</source>
<translation>Bit Rate</translation>
</message>
<message>
<source>Container</source>
<translation>Container</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Format Pixel</translation>
</message>
<message>
<source>WxH</source>
<translation>Lățime x Înălțime (LxÎ)</translation>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>Nu s-au găsit albume sau melodii aparținând acestui artist</translation>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Doar Subtitrări Text</translation>
</message>
<message>
<source>Aired</source>
<translation>Difuzat</translation>
</message>
<message>
<source>Only display text subtitles to minimize transcoding.</source>
<translation>Afișează doar subtitrările text pentru a minimiza transcodarea.</translation>
</message>
<message>
<source>Unplayed</source>
<translation>Nevizionat</translation>
</message>
</context>
<context>
<name></name>

1367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,23 +5,23 @@
"description": "Roku app for Jellyfin media server",
"dependencies": {
"@rokucommunity/bslib": "0.1.1",
"brighterscript-formatter": "1.7.5",
"brighterscript-formatter": "1.7.6",
"log": "npm:roku-log@0.11.1"
},
"devDependencies": {
"@rokucommunity/bslint": "0.8.25",
"brighterscript": "0.67.7",
"@rokucommunity/bslint": "0.8.26",
"brighterscript": "0.67.8",
"brighterscript-jsdocs-plugin": "0.7.3",
"clean-jsdoc-theme": "4.3.0",
"jsdoc": "4.0.3",
"jsdoc": "4.0.4",
"jshint": "2.13.6",
"markdownlint-cli2": "0.14.0",
"rimraf": "6.0.1",
"roku-deploy": "3.12.1",
"roku-deploy": "3.12.2",
"roku-log-bsc-plugin": "0.8.1",
"rooibos-roku": "5.14.0",
"ropm": "0.10.26",
"spellchecker-cli": "6.2.0",
"ropm": "0.10.27",
"spellchecker-cli": "7.0.0",
"undent": "0.1.0"
},
"scripts": {

View File

@ -262,6 +262,23 @@
}
]
},
{
"title": "Episode Next Up Behavior",
"description": "This controls what clicking OK on a Next Up episode does.",
"settingName": "ui.general.episodenextupbehavior",
"type": "radio",
"default": "episode",
"options": [
{
"title": "View Episode Details",
"id": "episode"
},
{
"title": "View Season Details",
"id": "season"
}
]
},
{
"title": "Hide Clock",
"description": "Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.",
@ -276,6 +293,27 @@
"type": "integer",
"default": "365"
},
{
"title": "OSD Remaining Time",
"description": "How is remaining time represented. Remaining time shows hours, minutes, and seconds remaining. Time of day shows what time the video will complete.",
"settingName": "ui.general.osdremainingtime",
"type": "radio",
"default": "remaining",
"options": [
{
"title": "Remaining Time",
"id": "remaining"
},
{
"title": "Time of Day",
"id": "timeofday"
},
{
"title": "Both",
"id": "both"
}
]
},
{
"title": "Rewatching Next Up",
"description": "Show already watched episodes in 'Next Up' sections.",
@ -283,6 +321,23 @@
"type": "bool",
"default": "false"
},
{
"title": "Row Layout",
"description": "Choose how rows are displayed on the home screen.",
"settingName": "ui.row.layout",
"type": "radio",
"default": "fullwidth",
"options": [
{
"title": "Original",
"id": "original"
},
{
"title": "Full Width",
"id": "fullwidth"
}
]
},
{
"title": "Show What's New Popup",
"description": "Show What's New popup when Jellyfin is updated to a new version.",

View File

@ -36,7 +36,7 @@ sub Main (args as dynamic) as void
sceneManager = CreateObject("roSGNode", "SceneManager")
sceneManager.observeField("dataReturned", m.port)
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager, launchSource: "" })
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") })
@ -307,22 +307,39 @@ sub Main (args as dynamic) as void
audio_stream_idx = selectedItem.selectedAudioStreamIndex
end if
launchSource = m.global.launchSource
m.global.launchSource = ""
selectedItem.selectedAudioStreamIndex = audio_stream_idx
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
localGlobal = m.global
' we only use this special steering logic from the home screen
if launchSource <> "home"
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
end if
else if localGlobal.session.user.settings["ui.general.episodenextupbehavior"] = "episode"
group = CreateMovieDetailsGroup(selectedItem)
else if localGlobal.session.user.settings["ui.general.episodenextupbehavior"] = "season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.json.SeasonId, selectedItem.id)
end if
else if selectedItemType = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json.id)
else if selectedItemType = "Season"
if isValid(selectedItem.json) and isValid(selectedItem.json.SeriesId) and isValid(selectedItem.id)
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id, "")
else
stopLoadingSpinner()
message_dialog(tr("Error loading Season"))
@ -863,7 +880,7 @@ sub Main (args as dynamic) as void
else if popupNode.returnData.indexselected = 3
' User chose Go to season
if isValid(selectedItem[0].json) and isValid(selectedItem[0].json.SeriesId) and isValid(selectedItem[0].json.seasonID)
CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID)
CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID, "")
else
stopLoadingSpinner()
message_dialog(tr("Error loading Season"))

View File

@ -646,7 +646,7 @@ function CreateSeriesDetailsGroup(seriesID as string) as dynamic
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
if seasonData <> invalid and m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] and seasonData.Items.Count() = 1
stopLoadingSpinner()
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id, "")
end if
' start building SeriesDetails view
group = CreateObject("roSGNode", "TVShowDetails")
@ -793,7 +793,7 @@ function CreateSeasonDetailsGroup(series as object, season as object) as dynamic
return group
end function
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as dynamic
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string, selectId as string) as dynamic
' validate parameters
if seriesID = "" or seasonID = "" then return invalid
@ -805,9 +805,11 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as
stopLoadingSpinner()
return invalid
end if
' start building SeasonDetails view
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
' push scene asap (to prevent extra button presses when retriving series/movie info)
group.seasonData = seasonMetaData.json
group.objects = TVEpisodes(seriesID, seasonID)
@ -821,6 +823,10 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as
' check for specials/extras for this season
group.extrasObjects = TVSeasonExtras(seasonID)
group.observeField("sceneReady", "onSceneReady")
group.selectItemId = selectId
' finished building SeasonDetails view
return group
end function

View File

@ -70,6 +70,24 @@ function secondsToHuman(totalSeconds as integer, addLeadingMinuteZero as boolean
return humanTime
end function
function secondsToEndTime(totalSeconds) as string
' Get the current time in seconds since midnight UTC (Unix Epoch Time)
currentUTCTime = CreateObject("roDateTime").AsSeconds()
' Calculate the target time in seconds by adding the number of seconds
targetTimeInSeconds = currentUTCTime + totalSeconds
' Create a new roDateTime object for the target time
targetDateTime = CreateObject("roDateTime")
targetDateTime.FromSeconds(targetTimeInSeconds)
targetDateTime.ToLocalTime()
formattedTime = formatTime(targetDateTime)
return formattedTime
end function
' Format time as 12 or 24 hour format based on system clock setting
function formatTime(time) as string
hours = time.getHours()