From 48899bead2a0c1e53eb518e45f6e41f53496b2c3 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 1 Aug 2020 14:48:48 +0200 Subject: [PATCH 01/31] Update Swiper to 6.1.1 --- package.json | 2 +- src/bundle.js | 4 ++-- yarn.lock | 30 +++++++++++++++--------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 17d175b7e7..b91d1004f6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.2", "sortablejs": "^1.10.2", - "swiper": "^5.4.5", + "swiper": "^6.1.1", "webcomponents.js": "^0.7.24", "whatwg-fetch": "^3.2.0" }, diff --git a/src/bundle.js b/src/bundle.js index ae2a59f0d5..3c21ed66a6 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -60,8 +60,8 @@ _define('resize-observer-polyfill', function() { }); // swiper -var swiper = require('swiper/js/swiper'); -require('swiper/css/swiper.min.css'); +var swiper = require('swiper/swiper-bundle'); +require('swiper/swiper-bundle.css'); _define('swiper', function() { return swiper; }); diff --git a/yarn.lock b/yarn.lock index 09181bfc78..ef66f04419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3510,12 +3510,12 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom7@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/dom7/-/dom7-2.1.5.tgz#a79411017800b31d8400070cdaebbfc92c1f6377" - integrity sha512-xnhwVgyOh3eD++/XGtH+5qBwYTgCm0aW91GFgPJ3XG+jlsRLyJivnbP0QmUBFhI+Oaz9FV0s7cxgXHezwOEBYA== +dom7@^3.0.0-alpha.7: + version "3.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/dom7/-/dom7-3.0.0-alpha.7.tgz#3b4ba156a83fa37fb3fa34b8ab40a1a41a56feb1" + integrity sha512-3epkQPsKsbk2Dixqqgm2DT/KzhiAPByjDK7emu6owwFLbM5UoiqWKgdsH+6PpMEgoeR6Ex/bW1UbOe0FWZU0zg== dependencies: - ssr-window "^2.0.0" + ssr-window "^3.0.0-alpha.1" domain-browser@^1.1.1: version "1.2.0" @@ -10462,10 +10462,10 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssr-window@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-2.0.0.tgz#98c301aef99523317f8d69618f0010791096efc4" - integrity sha512-NXzN+/HPObKAx191H3zKlYomE5WrVIkoCB5IaSdvKokxTpjBdWfr0RaP+1Z5KOfDT0ZVz+2tdtiBkhsEQ9p+0A== +ssr-window@^3.0.0-alpha.1, ssr-window@^3.0.0-alpha.4: + version "3.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-3.0.0-alpha.4.tgz#0c69a18c4305ecccdd8e11596155ca07b635f345" + integrity sha512-+dBRP/pZ+VyITxTzD0lMDzDwN/BmfUl8xi2e6t5Nz4+FqUphfcBLB1OOUSYCRNFB25rD3c8AJRYpY5rHTbL+kg== ssri@^6.0.1: version "6.0.1" @@ -11038,13 +11038,13 @@ svgo@^1.0.0, svgo@^1.3.2: unquote "~1.1.1" util.promisify "~1.0.0" -swiper@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-5.4.5.tgz#a350f654bf68426dbb651793824925512d223c0f" - integrity sha512-7QjA0XpdOmiMoClfaZ2lYN6ICHcMm72LXiY+NF4fQLFidigameaofvpjEEiTQuw3xm5eksG5hzkaRsjQX57vtA== +swiper@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-6.1.1.tgz#1246f28557dd33968dc43e926bc6e9e9a7b3850d" + integrity sha512-w6rmEUnpuSWvzuIDJ+nTi7YQ4+pvr++zUnBO2VxkzOZbzQzcMNKNw1yj0RFEok682IHDPCs3LXSl8zSQ+zDEdw== dependencies: - dom7 "^2.1.5" - ssr-window "^2.0.0" + dom7 "^3.0.0-alpha.7" + ssr-window "^3.0.0-alpha.4" symbol-observable@1.0.1: version "1.0.1" From 9b1ed7ce4f837b8e0209cf18aefe60fe2a4f0679 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Sat, 15 Aug 2020 11:30:36 -0400 Subject: [PATCH 02/31] Change all instances of currentTime to be in ms. --- src/components/nowPlayingBar/nowPlayingBar.js | 2 +- src/components/playback/playbackmanager.js | 14 +++----------- src/components/remotecontrol/remotecontrol.js | 2 +- src/components/syncPlay/syncPlayManager.js | 2 +- src/components/upnextdialog/upnextdialog.js | 2 +- src/controllers/playback/video/index.js | 2 +- src/plugins/chromecastPlayer/plugin.js | 4 ++-- src/plugins/sessionPlayer/plugin.js | 4 ++-- 8 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 7aa8c623b3..d2de2f60f2 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -701,7 +701,7 @@ import 'emby-ratingbutton'; const player = this; currentRuntimeTicks = playbackManager.duration(player); - updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks, playbackManager.getBufferedRanges(player)); + updateTimeDisplay(playbackManager.currentTime(player) * 10000, currentRuntimeTicks, playbackManager.getBufferedRanges(player)); } function releaseCurrentPlayer() { diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 8502b551af..a5951e2784 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1572,11 +1572,7 @@ class PlaybackManager { player = player || self._currentPlayer; if (player && !enableLocalPlaylistManagement(player)) { - if (player.isLocalPlayer) { - return player.seek((ticks || 0) / 10000); - } else { - return player.seek(ticks); - } + return player.seek(ticks); } changeStream(player, ticks); @@ -1585,11 +1581,7 @@ class PlaybackManager { self.seekRelative = function (offsetTicks, player) { player = player || self._currentPlayer; if (player && !enableLocalPlaylistManagement(player) && player.seekRelative) { - if (player.isLocalPlayer) { - return player.seekRelative((ticks || 0) / 10000); - } else { - return player.seekRelative(ticks); - } + return player.seekRelative(ticks); } const ticks = getCurrentTicks(player) + offsetTicks; @@ -3173,7 +3165,7 @@ class PlaybackManager { return player.currentTime(); } - return this.getCurrentTicks(player); + return this.getCurrentTicks(player) / 10000; } nextItem(player = this._currentPlayer) { diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 6048c918c7..ee02cfd45a 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -615,7 +615,7 @@ export default function () { lastUpdateTime = now; const player = this; currentRuntimeTicks = playbackManager.duration(player); - updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks); + updateTimeDisplay(playbackManager.currentTime(player) * 10000, currentRuntimeTicks); } } diff --git a/src/components/syncPlay/syncPlayManager.js b/src/components/syncPlay/syncPlayManager.js index 2366172a79..26d6cdbad1 100644 --- a/src/components/syncPlay/syncPlayManager.js +++ b/src/components/syncPlay/syncPlayManager.js @@ -741,7 +741,7 @@ class SyncPlayManager { const playAtTime = this.lastCommand.When; - const currentPositionTicks = playbackManager.currentTime(); + const currentPositionTicks = playbackManager.currentTime() * 10000; // Estimate PositionTicks on server const serverPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000; // Measure delay that needs to be recovered diff --git a/src/components/upnextdialog/upnextdialog.js b/src/components/upnextdialog/upnextdialog.js index e28bb03abe..69cc6512fe 100644 --- a/src/components/upnextdialog/upnextdialog.js +++ b/src/components/upnextdialog/upnextdialog.js @@ -256,7 +256,7 @@ import 'flexStyles'; const runtimeTicks = playbackManager.duration(options.player); if (runtimeTicks) { - const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player); + const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000; return Math.round(timeRemainingTicks / 10000); } diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 73540cd636..c8d385b6d8 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -693,7 +693,7 @@ import 'css!assets/css/videoosd'; lastUpdateTime = now; const player = this; currentRuntimeTicks = playbackManager.duration(player); - const currentTime = playbackManager.currentTime(player); + const currentTime = playbackManager.currentTime(player) * 10000; updateTimeDisplay(currentTime, currentRuntimeTicks, playbackManager.playbackStartTime(player), playbackManager.getBufferedRanges(player)); const item = currentItem; refreshProgramInfoIfNeeded(player, item); diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b7e6d05969..d3faea2ce8 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -950,12 +950,12 @@ class ChromecastPlayer { currentTime(val) { if (val != null) { - return this.seek(val); + return this.seek(val * 10000); } let state = this.lastPlayerData || {}; state = state.PlayState || {}; - return state.PositionTicks; + return state.PositionTicks / 10000; } duration() { diff --git a/src/plugins/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index c68e0d7a4a..fc795c2a6d 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -324,12 +324,12 @@ define(['playbackManager', 'events', 'serverNotifications', 'connectionManager'] SessionPlayer.prototype.currentTime = function (val) { if (val != null) { - return this.seek(val); + return this.seek(val * 10000); } var state = this.lastPlayerData || {}; state = state.PlayState || {}; - return state.PositionTicks; + return state.PositionTicks / 10000; }; SessionPlayer.prototype.duration = function () { From 7952b75ca24aaffeaac8834f24880e502f5e344e Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Sat, 15 Aug 2020 14:33:31 -0400 Subject: [PATCH 03/31] Show seek buttons when playing video on mobile. --- src/components/remotecontrol/remotecontrol.css | 15 ++++----------- src/components/remotecontrol/remotecontrol.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.css b/src/components/remotecontrol/remotecontrol.css index c260799585..1c31b4382b 100644 --- a/src/components/remotecontrol/remotecontrol.css +++ b/src/components/remotecontrol/remotecontrol.css @@ -222,18 +222,10 @@ margin: 0; } -.layout-mobile .nowPlayingSecondaryButtons .btnShuffleQueue { - display: none; -} - .layout-mobile .nowPlayingSecondaryButtons .volumecontrol { display: none; } -.layout-mobile .nowPlayingSecondaryButtons .btnRepeat { - display: none; -} - .layout-desktop .nowPlayingInfoButtons .btnRepeat, .layout-tv .nowPlayingInfoButtons .btnRepeat { display: none; @@ -362,7 +354,8 @@ border-radius: 0; } - .nowPlayingInfoButtons .btnRepeat { + .nowPlayingInfoButtons .btnRepeat, + .nowPlayingInfoButtons .btnRewind { position: absolute; left: 0; margin-left: 0; @@ -370,7 +363,8 @@ font-size: smaller; } - .nowPlayingInfoButtons .btnShuffleQueue { + .nowPlayingInfoButtons .btnShuffleQueue, + .nowPlayingInfoButtons .btnFastForward { position: absolute; right: 0; margin-right: 0; @@ -468,7 +462,6 @@ } @media all and (max-width: 63em) { - .nowPlayingSecondaryButtons .repeatToggleButton, .nowPlayingInfoButtons .playlist .listItemMediaInfo, .nowPlayingInfoButtons .btnStop { display: none !important; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 6048c918c7..6a1355047c 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -332,8 +332,14 @@ export default function () { buttonVisible(context.querySelector('.btnNextTrack'), item != null); buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); if (layoutManager.mobile) { - buttonVisible(context.querySelector('.btnRewind'), false); - buttonVisible(context.querySelector('.btnFastForward'), false); + const playingVideo = playbackManager.isPlayingVideo() && item != null; + const playingAudio = !playbackManager.isPlayingVideo() && item != null; + buttonVisible(context.querySelector('.btnRepeat'), playingAudio); + buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio); + buttonVisible(context.querySelector('.btnRewind'), playingVideo); + buttonVisible(context.querySelector('.btnFastForward'), playingVideo); + buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnShuffleQueue'), playingVideo); + buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnRepeat'), playingVideo); } else { buttonVisible(context.querySelector('.btnRewind'), item != null); buttonVisible(context.querySelector('.btnFastForward'), item != null); From dc162aca95711b119ddb1e331b86cd3666dd0467 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 19 Aug 2020 19:19:23 -0400 Subject: [PATCH 04/31] Use strict equality check. Co-authored-by: Matjaz Zavski --- src/components/remotecontrol/remotecontrol.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 6a1355047c..ed463f67fe 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -332,8 +332,8 @@ export default function () { buttonVisible(context.querySelector('.btnNextTrack'), item != null); buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); if (layoutManager.mobile) { - const playingVideo = playbackManager.isPlayingVideo() && item != null; - const playingAudio = !playbackManager.isPlayingVideo() && item != null; + const playingVideo = playbackManager.isPlayingVideo() && item !== null; + const playingAudio = !playbackManager.isPlayingVideo() && item !== null; buttonVisible(context.querySelector('.btnRepeat'), playingAudio); buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio); buttonVisible(context.querySelector('.btnRewind'), playingVideo); From 49edf39d52f166ae1dc62922b86b2a22b79eb2ff Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 02:27:24 -0400 Subject: [PATCH 05/31] Fix video osd hiding remote seek buttons. --- src/assets/css/videoosd.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 59a485468d..fe19dde605 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -248,8 +248,8 @@ } @media all and (max-width: 30em) { - .btnFastForward, - .btnRewind, + .osdControls .btnFastForward, + .osdControls .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; @@ -281,3 +281,4 @@ display: none !important; } } + From 6523de60b1634b62311adbdb5948ade0282dd5f2 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 02:32:48 -0400 Subject: [PATCH 06/31] Remove empty line for stylelint. --- src/assets/css/videoosd.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index fe19dde605..2a28e9cff8 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -281,4 +281,3 @@ display: none !important; } } - From b8cf026bad79439024409e7466338282f3c131c7 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 25 Aug 2020 15:44:01 +0100 Subject: [PATCH 07/31] Fix broken item details menu on TV Shows --- src/controllers/itemDetails/index.js | 3561 +++++++++++++------------- 1 file changed, 1779 insertions(+), 1782 deletions(-) diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 63225f6793..74872f1261 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -29,2006 +29,2003 @@ import 'emby-ratingbutton'; import 'emby-scroller'; import 'emby-select'; -/* eslint-disable indent */ +function getPromise(apiClient, params) { + const id = params.id; - function getPromise(apiClient, params) { - const id = params.id; - - if (id) { - return apiClient.getItem(apiClient.getCurrentUserId(), id); - } - - if (params.seriesTimerId) { - return apiClient.getLiveTvSeriesTimer(params.seriesTimerId); - } - - if (params.genre) { - return apiClient.getGenre(params.genre, apiClient.getCurrentUserId()); - } - - if (params.musicgenre) { - return apiClient.getMusicGenre(params.musicgenre, apiClient.getCurrentUserId()); - } - - if (params.musicartist) { - return apiClient.getArtist(params.musicartist, apiClient.getCurrentUserId()); - } - - throw new Error('Invalid request'); + if (id) { + return apiClient.getItem(apiClient.getCurrentUserId(), id); } - function hideAll(page, className, show) { - for (const elem of page.querySelectorAll('.' + className)) { - if (show) { - elem.classList.remove('hide'); - } else { - elem.classList.add('hide'); - } + if (params.seriesTimerId) { + return apiClient.getLiveTvSeriesTimer(params.seriesTimerId); + } + + if (params.genre) { + return apiClient.getGenre(params.genre, apiClient.getCurrentUserId()); + } + + if (params.musicgenre) { + return apiClient.getMusicGenre(params.musicgenre, apiClient.getCurrentUserId()); + } + + if (params.musicartist) { + return apiClient.getArtist(params.musicartist, apiClient.getCurrentUserId()); + } + + throw new Error('Invalid request'); +} + +function hideAll(page, className, show) { + for (const elem of page.querySelectorAll('.' + className)) { + if (show) { + elem.classList.remove('hide'); + } else { + elem.classList.add('hide'); } } +} - function getContextMenuOptions(item, user, button) { - return { - item: item, - open: false, - play: false, - playAllFromHere: false, - queueAllFromHere: false, - positionTo: button, - cancelTimer: false, - record: false, - deleteItem: item.CanDelete === true, - shuffle: false, - instantMix: false, - user: user, - share: true - }; - } +function getContextMenuOptions(item, user, button) { + return { + item: item, + open: false, + play: false, + playAllFromHere: false, + queueAllFromHere: false, + positionTo: button, + cancelTimer: false, + record: false, + deleteItem: item.CanDelete === true, + shuffle: false, + instantMix: false, + user: user, + share: true + }; +} - function getProgramScheduleHtml(items) { - let html = ''; +function getProgramScheduleHtml(items) { + let html = ''; - html += '
'; - html += listView.getListViewHtml({ - items: items, - enableUserDataButtons: false, - image: true, - imageSource: 'channel', - showProgramDateTime: true, - showChannel: false, - mediaInfo: false, - action: 'none', - moreButton: false, - recordButton: false - }); + html += '
'; + html += listView.getListViewHtml({ + items: items, + enableUserDataButtons: false, + image: true, + imageSource: 'channel', + showProgramDateTime: true, + showChannel: false, + mediaInfo: false, + action: 'none', + moreButton: false, + recordButton: false + }); - html += '
'; + html += '
'; - return html; - } + return html; +} - function renderSeriesTimerSchedule(page, apiClient, seriesTimerId) { - apiClient.getLiveTvTimers({ - UserId: apiClient.getCurrentUserId(), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - SortBy: 'StartDate', - EnableTotalRecordCount: false, - EnableUserData: false, - SeriesTimerId: seriesTimerId, - Fields: 'ChannelInfo,ChannelImage' - }).then(function (result) { - if (result.Items.length && result.Items[0].SeriesTimerId != seriesTimerId) { - result.Items = []; - } - - const html = getProgramScheduleHtml(result.Items); - const scheduleTab = page.querySelector('.seriesTimerSchedule'); - scheduleTab.innerHTML = html; - imageLoader.lazyChildren(scheduleTab); - }); - } - - function renderTimerEditor(page, item, apiClient, user) { - if (item.Type !== 'Recording' || !user.Policy.EnableLiveTvManagement || !item.TimerId || item.Status !== 'InProgress') { - return void hideAll(page, 'btnCancelTimer'); +function renderSeriesTimerSchedule(page, apiClient, seriesTimerId) { + apiClient.getLiveTvTimers({ + UserId: apiClient.getCurrentUserId(), + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + SortBy: 'StartDate', + EnableTotalRecordCount: false, + EnableUserData: false, + SeriesTimerId: seriesTimerId, + Fields: 'ChannelInfo,ChannelImage' + }).then(function (result) { + if (result.Items.length && result.Items[0].SeriesTimerId != seriesTimerId) { + result.Items = []; } - hideAll(page, 'btnCancelTimer', true); + const html = getProgramScheduleHtml(result.Items); + const scheduleTab = page.querySelector('.seriesTimerSchedule'); + scheduleTab.innerHTML = html; + imageLoader.lazyChildren(scheduleTab); + }); +} + +function renderTimerEditor(page, item, apiClient, user) { + if (item.Type !== 'Recording' || !user.Policy.EnableLiveTvManagement || !item.TimerId || item.Status !== 'InProgress') { + return void hideAll(page, 'btnCancelTimer'); } - function renderSeriesTimerEditor(page, item, apiClient, user) { - if (item.Type !== 'SeriesTimer') { - return void hideAll(page, 'btnCancelSeriesTimer'); - } + hideAll(page, 'btnCancelTimer', true); +} - if (user.Policy.EnableLiveTvManagement) { - import('seriesRecordingEditor').then(({default: seriesRecordingEditor}) => { - seriesRecordingEditor.embed(item, apiClient.serverId(), { - context: page.querySelector('.seriesRecordingEditor') - }); - }); - - page.querySelector('.seriesTimerScheduleSection').classList.remove('hide'); - hideAll(page, 'btnCancelSeriesTimer', true); - return void renderSeriesTimerSchedule(page, apiClient, item.Id); - } - - page.querySelector('.seriesTimerScheduleSection').classList.add('hide'); +function renderSeriesTimerEditor(page, item, apiClient, user) { + if (item.Type !== 'SeriesTimer') { return void hideAll(page, 'btnCancelSeriesTimer'); } - function renderTrackSelections(page, instance, item, forceReload) { - const select = page.querySelector('.selectSource'); - - if (!item.MediaSources || !itemHelper.supportsMediaSourceSelection(item) || playbackManager.getSupportedCommands().indexOf('PlayMediaSource') === -1 || !playbackManager.canPlay(item)) { - page.querySelector('.trackSelections').classList.add('hide'); - select.innerHTML = ''; - page.querySelector('.selectVideo').innerHTML = ''; - page.querySelector('.selectAudio').innerHTML = ''; - page.querySelector('.selectSubtitles').innerHTML = ''; - return; - } - - const mediaSources = item.MediaSources; - instance._currentPlaybackMediaSources = mediaSources; - - page.querySelector('.trackSelections').classList.remove('hide'); - select.setLabel(globalize.translate('LabelVersion')); - - const currentValue = select.value; - - const selectedId = mediaSources[0].Id; - select.innerHTML = mediaSources.map(function (v) { - const selected = v.Id === selectedId ? ' selected' : ''; - return ''; - }).join(''); - - if (mediaSources.length > 1) { - page.querySelector('.selectSourceContainer').classList.remove('hide'); - } else { - page.querySelector('.selectSourceContainer').classList.add('hide'); - } - - if (select.value !== currentValue || forceReload) { - renderVideoSelections(page, mediaSources); - renderAudioSelections(page, mediaSources); - renderSubtitleSelections(page, mediaSources); - } - } - - function renderVideoSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Video'; + if (user.Policy.EnableLiveTvManagement) { + import('seriesRecordingEditor').then(({ default: seriesRecordingEditor }) => { + seriesRecordingEditor.embed(item, apiClient.serverId(), { + context: page.querySelector('.seriesRecordingEditor') + }); }); - const select = page.querySelector('.selectVideo'); - select.setLabel(globalize.translate('LabelVideo')); - const selectedId = tracks.length ? tracks[0].Index : -1; - select.innerHTML = tracks.map(function (v) { - const selected = v.Index === selectedId ? ' selected' : ''; - const titleParts = []; - const resolutionText = mediaInfo.getResolutionText(v); + page.querySelector('.seriesTimerScheduleSection').classList.remove('hide'); + hideAll(page, 'btnCancelSeriesTimer', true); + return void renderSeriesTimerSchedule(page, apiClient, item.Id); + } - if (resolutionText) { - titleParts.push(resolutionText); - } + page.querySelector('.seriesTimerScheduleSection').classList.add('hide'); + return void hideAll(page, 'btnCancelSeriesTimer'); +} - if (v.Codec) { - titleParts.push(v.Codec.toUpperCase()); - } +function renderTrackSelections(page, instance, item, forceReload) { + const select = page.querySelector('.selectSource'); - return ''; - }).join(''); + if (!item.MediaSources || !itemHelper.supportsMediaSourceSelection(item) || playbackManager.getSupportedCommands().indexOf('PlayMediaSource') === -1 || !playbackManager.canPlay(item)) { + page.querySelector('.trackSelections').classList.add('hide'); + select.innerHTML = ''; + page.querySelector('.selectVideo').innerHTML = ''; + page.querySelector('.selectAudio').innerHTML = ''; + page.querySelector('.selectSubtitles').innerHTML = ''; + return; + } + + const mediaSources = item.MediaSources; + instance._currentPlaybackMediaSources = mediaSources; + + page.querySelector('.trackSelections').classList.remove('hide'); + select.setLabel(globalize.translate('LabelVersion')); + + const currentValue = select.value; + + const selectedId = mediaSources[0].Id; + select.innerHTML = mediaSources.map(function (v) { + const selected = v.Id === selectedId ? ' selected' : ''; + return ''; + }).join(''); + + if (mediaSources.length > 1) { + page.querySelector('.selectSourceContainer').classList.remove('hide'); + } else { + page.querySelector('.selectSourceContainer').classList.add('hide'); + } + + if (select.value !== currentValue || forceReload) { + renderVideoSelections(page, mediaSources); + renderAudioSelections(page, mediaSources); + renderSubtitleSelections(page, mediaSources); + } +} + +function renderVideoSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Video'; + }); + + const select = page.querySelector('.selectVideo'); + select.setLabel(globalize.translate('LabelVideo')); + const selectedId = tracks.length ? tracks[0].Index : -1; + select.innerHTML = tracks.map(function (v) { + const selected = v.Index === selectedId ? ' selected' : ''; + const titleParts = []; + const resolutionText = mediaInfo.getResolutionText(v); + + if (resolutionText) { + titleParts.push(resolutionText); + } + + if (v.Codec) { + titleParts.push(v.Codec.toUpperCase()); + } + + return ''; + }).join(''); + select.setAttribute('disabled', 'disabled'); + + if (tracks.length) { + page.querySelector('.selectVideoContainer').classList.remove('hide'); + } else { + page.querySelector('.selectVideoContainer').classList.add('hide'); + } +} + +function renderAudioSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Audio'; + }); + const select = page.querySelector('.selectAudio'); + select.setLabel(globalize.translate('Audio')); + const selectedId = mediaSource.DefaultAudioStreamIndex; + select.innerHTML = tracks.map(function (v) { + const selected = v.Index === selectedId ? ' selected' : ''; + return ''; + }).join(''); + + if (tracks.length > 1) { + select.removeAttribute('disabled'); + } else { select.setAttribute('disabled', 'disabled'); - - if (tracks.length) { - page.querySelector('.selectVideoContainer').classList.remove('hide'); - } else { - page.querySelector('.selectVideoContainer').classList.add('hide'); - } } - function renderAudioSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Audio'; - }); - const select = page.querySelector('.selectAudio'); - select.setLabel(globalize.translate('Audio')); - const selectedId = mediaSource.DefaultAudioStreamIndex; - select.innerHTML = tracks.map(function (v) { - const selected = v.Index === selectedId ? ' selected' : ''; + if (tracks.length) { + page.querySelector('.selectAudioContainer').classList.remove('hide'); + } else { + page.querySelector('.selectAudioContainer').classList.add('hide'); + } +} + +function renderSubtitleSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Subtitle'; + }); + const select = page.querySelector('.selectSubtitles'); + select.setLabel(globalize.translate('Subtitles')); + const selectedId = mediaSource.DefaultSubtitleStreamIndex == null ? -1 : mediaSource.DefaultSubtitleStreamIndex; + + const videoTracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Video'; + }); + + // This only makes sense on Video items + if (videoTracks.length) { + let selected = selectedId === -1 ? ' selected' : ''; + select.innerHTML = '' + tracks.map(function (v) { + selected = v.Index === selectedId ? ' selected' : ''; return ''; }).join(''); - if (tracks.length > 1) { + if (tracks.length > 0) { select.removeAttribute('disabled'); } else { select.setAttribute('disabled', 'disabled'); } - if (tracks.length) { - page.querySelector('.selectAudioContainer').classList.remove('hide'); - } else { - page.querySelector('.selectAudioContainer').classList.add('hide'); - } + page.querySelector('.selectSubtitlesContainer').classList.remove('hide'); + } else { + select.innerHTML = ''; + page.querySelector('.selectSubtitlesContainer').classList.add('hide'); } +} - function renderSubtitleSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Subtitle'; - }); - const select = page.querySelector('.selectSubtitles'); - select.setLabel(globalize.translate('Subtitles')); - const selectedId = mediaSource.DefaultSubtitleStreamIndex == null ? -1 : mediaSource.DefaultSubtitleStreamIndex; +function reloadPlayButtons(page, item) { + let canPlay = false; - const videoTracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Video'; - }); + if (item.Type == 'Program') { + const now = new Date(); - // This only makes sense on Video items - if (videoTracks.length) { - let selected = selectedId === -1 ? ' selected' : ''; - select.innerHTML = '' + tracks.map(function (v) { - selected = v.Index === selectedId ? ' selected' : ''; - return ''; - }).join(''); - - if (tracks.length > 0) { - select.removeAttribute('disabled'); - } else { - select.setAttribute('disabled', 'disabled'); - } - - page.querySelector('.selectSubtitlesContainer').classList.remove('hide'); - } else { - select.innerHTML = ''; - page.querySelector('.selectSubtitlesContainer').classList.add('hide'); - } - } - - function reloadPlayButtons(page, item) { - let canPlay = false; - - if (item.Type == 'Program') { - const now = new Date(); - - if (now >= datetime.parseISO8601Date(item.StartDate, true) && now < datetime.parseISO8601Date(item.EndDate, true)) { - hideAll(page, 'btnPlay', true); - canPlay = true; - } else { - hideAll(page, 'btnPlay'); - } - - hideAll(page, 'btnResume'); - hideAll(page, 'btnInstantMix'); - hideAll(page, 'btnShuffle'); - } else if (playbackManager.canPlay(item)) { + if (now >= datetime.parseISO8601Date(item.StartDate, true) && now < datetime.parseISO8601Date(item.EndDate, true)) { hideAll(page, 'btnPlay', true); - const enableInstantMix = ['Audio', 'MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; - hideAll(page, 'btnInstantMix', enableInstantMix); - const enableShuffle = item.IsFolder || ['MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; - hideAll(page, 'btnShuffle', enableShuffle); canPlay = true; - - const isResumable = item.UserData && item.UserData.PlaybackPositionTicks > 0; - hideAll(page, 'btnResume', isResumable); - - if (isResumable) { - for (const elem of page.querySelectorAll('.btnPlay')) { - elem.querySelector('.detailButton-icon').classList.replace('play_arrow', 'replay'); - } - } } else { hideAll(page, 'btnPlay'); - hideAll(page, 'btnResume'); - hideAll(page, 'btnInstantMix'); - hideAll(page, 'btnShuffle'); } - return canPlay; + hideAll(page, 'btnResume'); + hideAll(page, 'btnInstantMix'); + hideAll(page, 'btnShuffle'); + } else if (playbackManager.canPlay(item)) { + hideAll(page, 'btnPlay', true); + const enableInstantMix = ['Audio', 'MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; + hideAll(page, 'btnInstantMix', enableInstantMix); + const enableShuffle = item.IsFolder || ['MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; + hideAll(page, 'btnShuffle', enableShuffle); + canPlay = true; + + const isResumable = item.UserData && item.UserData.PlaybackPositionTicks > 0; + hideAll(page, 'btnResume', isResumable); + + if (isResumable) { + for (const elem of page.querySelectorAll('.btnPlay')) { + elem.querySelector('.detailButton-icon').classList.replace('play_arrow', 'replay'); + } + } + } else { + hideAll(page, 'btnPlay'); + hideAll(page, 'btnResume'); + hideAll(page, 'btnInstantMix'); + hideAll(page, 'btnShuffle'); } - function reloadUserDataButtons(page, item) { - let i; - let length; - const btnPlaystates = page.querySelectorAll('.btnPlaystate'); + return canPlay; +} - for (i = 0, length = btnPlaystates.length; i < length; i++) { - const btnPlaystate = btnPlaystates[i]; +function reloadUserDataButtons(page, item) { + let i; + let length; + const btnPlaystates = page.querySelectorAll('.btnPlaystate'); - if (itemHelper.canMarkPlayed(item)) { - btnPlaystate.classList.remove('hide'); - btnPlaystate.setItem(item); - } else { - btnPlaystate.classList.add('hide'); - btnPlaystate.setItem(null); - } - } + for (i = 0, length = btnPlaystates.length; i < length; i++) { + const btnPlaystate = btnPlaystates[i]; - const btnUserRatings = page.querySelectorAll('.btnUserRating'); - - for (i = 0, length = btnUserRatings.length; i < length; i++) { - const btnUserRating = btnUserRatings[i]; - - if (itemHelper.canRate(item)) { - btnUserRating.classList.remove('hide'); - btnUserRating.setItem(item); - } else { - btnUserRating.classList.add('hide'); - btnUserRating.setItem(null); - } + if (itemHelper.canMarkPlayed(item)) { + btnPlaystate.classList.remove('hide'); + btnPlaystate.setItem(item); + } else { + btnPlaystate.classList.add('hide'); + btnPlaystate.setItem(null); } } - function getArtistLinksHtml(artists, serverId, context) { - const html = []; + const btnUserRatings = page.querySelectorAll('.btnUserRating'); - for (const artist of artists) { - const href = appRouter.getRouteUrl(artist, { - context: context, - itemType: 'MusicArtist', - serverId: serverId - }); - html.push('' + artist.Name + ''); + for (i = 0, length = btnUserRatings.length; i < length; i++) { + const btnUserRating = btnUserRatings[i]; + + if (itemHelper.canRate(item)) { + btnUserRating.classList.remove('hide'); + btnUserRating.setItem(item); + } else { + btnUserRating.classList.add('hide'); + btnUserRating.setItem(null); } - - return html.join(' / '); } +} - /** - * Renders the item's name block - * @param {Object} item - Item used to render the name. - * @param {HTMLDivElement} container - Container to render the information into. - * @param {Object} context - Application context. - */ - function renderName(item, container, context) { - let parentRoute; - const parentNameHtml = []; - let parentNameLast = false; +function getArtistLinksHtml(artists, serverId, context) { + const html = []; - if (item.AlbumArtists) { - parentNameHtml.push(getArtistLinksHtml(item.AlbumArtists, item.ServerId, context)); - parentNameLast = true; - } else if (item.ArtistItems && item.ArtistItems.length && item.Type === 'MusicVideo') { - parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); - parentNameLast = true; - } else if (item.SeriesName && item.Type === 'Episode') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeriesId, - Name: item.SeriesName, - Type: 'Series', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeriesName + ''); - } else if (item.IsSeries || item.EpisodeTitle) { - parentNameHtml.push(item.Name); - } - - if (item.SeriesName && item.Type === 'Season') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeriesId, - Name: item.SeriesName, - Type: 'Series', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeriesName + ''); - } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeasonId, - Name: item.SeasonName, - Type: 'Season', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeasonName + ''); - } else if (item.ParentIndexNumber != null && item.IsSeries) { - parentNameHtml.push(item.SeasonName || 'S' + item.ParentIndexNumber); - } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { - parentRoute = appRouter.getRouteUrl({ - Id: item.AlbumId, - Name: item.Album, - Type: 'MusicAlbum', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.Album + ''); - } else if (item.Album) { - parentNameHtml.push(item.Album); - } - - // FIXME: This whole section needs some refactoring, so it becames easier to scale across all form factors. See GH #1022 - let html = ''; - const tvShowHtml = parentNameHtml[0]; - const tvSeasonHtml = parentNameHtml[1]; - - if (parentNameHtml.length) { - if (parentNameLast) { - // Music - if (layoutManager.mobile) { - html = '

' + parentNameHtml.join('
') + '

'; - } else { - html = '

' + parentNameHtml.join(' - ') + '

'; - } - } else { - html = '

' + tvShowHtml + '

'; - } - } - - const name = itemHelper.getDisplayName(item, { - includeParentInfo: false + for (const artist of artists) { + const href = appRouter.getRouteUrl(artist, { + context: context, + itemType: 'MusicArtist', + serverId: serverId }); - - if (html && !parentNameLast) { - if (tvSeasonHtml) { - html += '

' + tvSeasonHtml + ' - ' + name + '

'; - } else { - html += '

' + name + '

'; - } - } else if (item.OriginalTitle && item.OriginalTitle != item.Name) { - html = '

' + name + '

' + html; - } else { - html = '

' + name + '

' + html; - } - - if (item.OriginalTitle && item.OriginalTitle != item.Name) { - html += '

' + item.OriginalTitle + '

'; - } - - container.innerHTML = html; - - if (html.length) { - container.classList.remove('hide'); - } else { - container.classList.add('hide'); - } + html.push('' + artist.Name + ''); } - function setTrailerButtonVisibility(page, item) { - if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { - hideAll(page, 'btnPlayTrailer', true); - } else { - hideAll(page, 'btnPlayTrailer'); - } - } + return html.join(' / '); +} - function renderBackdrop(item) { - if (dom.getWindowSize().innerWidth >= 1000) { - backdrop.setBackdrops([item]); - } else { - backdrop.clearBackdrop(); - } - } +/** + * Renders the item's name block + * @param {Object} item - Item used to render the name. + * @param {HTMLDivElement} container - Container to render the information into. + * @param {Object} context - Application context. + */ +function renderName(item, container, context) { + let parentRoute; + const parentNameHtml = []; + let parentNameLast = false; - function renderDetailPageBackdrop(page, item, apiClient) { - let imgUrl; - let hasbackdrop = false; - const itemBackdropElement = page.querySelector('#itemBackdrop'); - - if (!layoutManager.mobile && !userSettings.detailsBanner()) { - return false; - } - - if (item.BackdropImageTags && item.BackdropImageTags.length) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.BackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { - imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.ParentBackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ImageTags && item.ImageTags.Primary) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Primary', - maxWidth: dom.getScreenWidth(), - tag: item.ImageTags.Primary - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else { - itemBackdropElement.style.backgroundImage = ''; - } - - return hasbackdrop; - } - - function reloadFromItem(instance, page, params, item, user) { - const apiClient = connectionManager.getApiClient(item.ServerId); - - Emby.Page.setTitle(''); - - // Start rendering the artwork first - renderImage(page, item); - renderLogo(page, item, apiClient); - renderBackdrop(item); - renderDetailPageBackdrop(page, item, apiClient); - - // Render the main information for the item - page.querySelector('.detailPagePrimaryContainer').classList.add('detailRibbon'); - renderName(item, page.querySelector('.nameContainer'), params.context); - renderDetails(page, item, apiClient, params.context); - renderTrackSelections(page, instance, item); - - renderSeriesTimerEditor(page, item, apiClient, user); - renderTimerEditor(page, item, apiClient, user); - setInitialCollapsibleState(page, item, apiClient, params.context, user); - const canPlay = reloadPlayButtons(page, item); - - if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { - hideAll(page, 'btnPlayTrailer', true); - } else { - hideAll(page, 'btnPlayTrailer'); - } - - setTrailerButtonVisibility(page, item); - - if (item.Type !== 'Program' || canPlay) { - hideAll(page, 'mainDetailButtons', true); - } else { - hideAll(page, 'mainDetailButtons'); - } - - showRecordingFields(instance, page, item, user); - const groupedVersions = (item.MediaSources || []).filter(function (g) { - return g.Type == 'Grouping'; + if (item.AlbumArtists) { + parentNameHtml.push(getArtistLinksHtml(item.AlbumArtists, item.ServerId, context)); + parentNameLast = true; + } else if (item.ArtistItems && item.ArtistItems.length && item.Type === 'MusicVideo') { + parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); + parentNameLast = true; + } else if (item.SeriesName && item.Type === 'Episode') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeriesId, + Name: item.SeriesName, + Type: 'Series', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context }); + parentNameHtml.push('' + item.SeriesName + ''); + } else if (item.IsSeries || item.EpisodeTitle) { + parentNameHtml.push(item.Name); + } - if (user.Policy.IsAdministrator && groupedVersions.length) { - page.querySelector('.btnSplitVersions').classList.remove('hide'); - } else { - page.querySelector('.btnSplitVersions').classList.add('hide'); - } + if (item.SeriesName && item.Type === 'Season') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeriesId, + Name: item.SeriesName, + Type: 'Series', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.SeriesName + ''); + } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeasonId, + Name: item.SeasonName, + Type: 'Season', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.SeasonName + ''); + } else if (item.ParentIndexNumber != null && item.IsSeries) { + parentNameHtml.push(item.SeasonName || 'S' + item.ParentIndexNumber); + } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { + parentRoute = appRouter.getRouteUrl({ + Id: item.AlbumId, + Name: item.Album, + Type: 'MusicAlbum', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.Album + ''); + } else if (item.Album) { + parentNameHtml.push(item.Album); + } - if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) { - hideAll(page, 'btnMoreCommands', true); - } else { - hideAll(page, 'btnMoreCommands'); - } + // FIXME: This whole section needs some refactoring, so it becames easier to scale across all form factors. See GH #1022 + let html = ''; + const tvShowHtml = parentNameHtml[0]; + const tvSeasonHtml = parentNameHtml[1]; - const itemBirthday = page.querySelector('#itemBirthday'); - - if (item.Type == 'Person' && item.PremiereDate) { - try { - const birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString(); - itemBirthday.classList.remove('hide'); - itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday); - } catch (err) { - itemBirthday.classList.add('hide'); + if (parentNameHtml.length) { + if (parentNameLast) { + // Music + if (layoutManager.mobile) { + html = '

' + parentNameHtml.join('
') + '

'; + } else { + html = '

' + parentNameHtml.join(' - ') + '

'; } } else { + html = '

' + tvShowHtml + '

'; + } + } + + const name = itemHelper.getDisplayName(item, { + includeParentInfo: false + }); + + if (html && !parentNameLast) { + if (tvSeasonHtml) { + html += '

' + tvSeasonHtml + ' - ' + name + '

'; + } else { + html += '

' + name + '

'; + } + } else if (item.OriginalTitle && item.OriginalTitle != item.Name) { + html = '

' + name + '

' + html; + } else { + html = '

' + name + '

' + html; + } + + if (item.OriginalTitle && item.OriginalTitle != item.Name) { + html += '

' + item.OriginalTitle + '

'; + } + + container.innerHTML = html; + + if (html.length) { + container.classList.remove('hide'); + } else { + container.classList.add('hide'); + } +} + +function setTrailerButtonVisibility(page, item) { + if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { + hideAll(page, 'btnPlayTrailer', true); + } else { + hideAll(page, 'btnPlayTrailer'); + } +} + +function renderBackdrop(item) { + if (dom.getWindowSize().innerWidth >= 1000) { + backdrop.setBackdrops([item]); + } else { + backdrop.clearBackdrop(); + } +} + +function renderDetailPageBackdrop(page, item, apiClient) { + let imgUrl; + let hasbackdrop = false; + const itemBackdropElement = page.querySelector('#itemBackdrop'); + + if (!layoutManager.mobile && !userSettings.detailsBanner()) { + return false; + } + + if (item.BackdropImageTags && item.BackdropImageTags.length) { + imgUrl = apiClient.getScaledImageUrl(item.Id, { + type: 'Backdrop', + maxWidth: dom.getScreenWidth(), + index: 0, + tag: item.BackdropImageTags[0] + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { + imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { + type: 'Backdrop', + maxWidth: dom.getScreenWidth(), + index: 0, + tag: item.ParentBackdropImageTags[0] + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else if (item.ImageTags && item.ImageTags.Primary) { + imgUrl = apiClient.getScaledImageUrl(item.Id, { + type: 'Primary', + maxWidth: dom.getScreenWidth(), + tag: item.ImageTags.Primary + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else { + itemBackdropElement.style.backgroundImage = ''; + } + + return hasbackdrop; +} + +function reloadFromItem(instance, page, params, item, user) { + const apiClient = connectionManager.getApiClient(item.ServerId); + + Emby.Page.setTitle(''); + + // Start rendering the artwork first + renderImage(page, item); + renderLogo(page, item, apiClient); + renderBackdrop(item); + renderDetailPageBackdrop(page, item, apiClient); + + // Render the main information for the item + page.querySelector('.detailPagePrimaryContainer').classList.add('detailRibbon'); + renderName(item, page.querySelector('.nameContainer'), params.context); + renderDetails(page, item, apiClient, params.context); + renderTrackSelections(page, instance, item); + + renderSeriesTimerEditor(page, item, apiClient, user); + renderTimerEditor(page, item, apiClient, user); + setInitialCollapsibleState(page, item, apiClient, params.context, user); + const canPlay = reloadPlayButtons(page, item); + + if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { + hideAll(page, 'btnPlayTrailer', true); + } else { + hideAll(page, 'btnPlayTrailer'); + } + + setTrailerButtonVisibility(page, item); + + if (item.Type !== 'Program' || canPlay) { + hideAll(page, 'mainDetailButtons', true); + } else { + hideAll(page, 'mainDetailButtons'); + } + + showRecordingFields(instance, page, item, user); + const groupedVersions = (item.MediaSources || []).filter(function (g) { + return g.Type == 'Grouping'; + }); + + if (user.Policy.IsAdministrator && groupedVersions.length) { + page.querySelector('.btnSplitVersions').classList.remove('hide'); + } else { + page.querySelector('.btnSplitVersions').classList.add('hide'); + } + + if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) { + hideAll(page, 'btnMoreCommands', true); + } else { + hideAll(page, 'btnMoreCommands'); + } + + const itemBirthday = page.querySelector('#itemBirthday'); + + if (item.Type == 'Person' && item.PremiereDate) { + try { + const birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString(); + itemBirthday.classList.remove('hide'); + itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday); + } catch (err) { itemBirthday.classList.add('hide'); } + } else { + itemBirthday.classList.add('hide'); + } - const itemDeathDate = page.querySelector('#itemDeathDate'); + const itemDeathDate = page.querySelector('#itemDeathDate'); - if (item.Type == 'Person' && item.EndDate) { - try { - const deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString(); - itemDeathDate.classList.remove('hide'); - itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday); - } catch (err) { - itemDeathDate.classList.add('hide'); - } - } else { + if (item.Type == 'Person' && item.EndDate) { + try { + const deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString(); + itemDeathDate.classList.remove('hide'); + itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday); + } catch (err) { itemDeathDate.classList.add('hide'); } - - const itemBirthLocation = page.querySelector('#itemBirthLocation'); - - if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) { - const gmap = '' + item.ProductionLocations[0] + ''; - itemBirthLocation.classList.remove('hide'); - itemBirthLocation.innerHTML = globalize.translate('BirthPlaceValue', gmap); - } else { - itemBirthLocation.classList.add('hide'); - } - - setPeopleHeader(page, item); - loading.hide(); - - if (item.Type === 'Book' && item.CanDownload && appHost.supports('filedownload')) { - hideAll(page, 'btnDownload', true); - } - - import('autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); + } else { + itemDeathDate.classList.add('hide'); } - function logoImageUrl(item, apiClient, options) { - options = options || {}; - options.type = 'Logo'; + const itemBirthLocation = page.querySelector('#itemBirthLocation'); - if (item.ImageTags && item.ImageTags.Logo) { - options.tag = item.ImageTags.Logo; - return apiClient.getScaledImageUrl(item.Id, options); - } - - if (item.ParentLogoImageTag) { - options.tag = item.ParentLogoImageTag; - return apiClient.getScaledImageUrl(item.ParentLogoItemId, options); - } - - return null; + if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) { + const gmap = '' + item.ProductionLocations[0] + ''; + itemBirthLocation.classList.remove('hide'); + itemBirthLocation.innerHTML = globalize.translate('BirthPlaceValue', gmap); + } else { + itemBirthLocation.classList.add('hide'); } - function renderLogo(page, item, apiClient) { - const detailLogo = page.querySelector('.detailLogo'); + setPeopleHeader(page, item); + loading.hide(); - const url = logoImageUrl(item, apiClient, {}); - - if (!layoutManager.mobile && !userSettings.enableBackdrops()) { - detailLogo.classList.add('hide'); - } else if (url) { - detailLogo.classList.remove('hide'); - imageLoader.setLazyImage(detailLogo, url); - } else { - detailLogo.classList.add('hide'); - } + if (item.Type === 'Book' && item.CanDownload && appHost.supports('filedownload')) { + hideAll(page, 'btnDownload', true); } - function showRecordingFields(instance, page, item, user) { - if (!instance.currentRecordingFields) { - const recordingFieldsElement = page.querySelector('.recordingFields'); + import('autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); +} - if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) { - import('recordingFields').then(({default: recordingFields}) => { - instance.currentRecordingFields = new recordingFields({ - parent: recordingFieldsElement, - programId: item.Id, - serverId: item.ServerId - }); - recordingFieldsElement.classList.remove('hide'); +function logoImageUrl(item, apiClient, options) { + options = options || {}; + options.type = 'Logo'; + + if (item.ImageTags && item.ImageTags.Logo) { + options.tag = item.ImageTags.Logo; + return apiClient.getScaledImageUrl(item.Id, options); + } + + if (item.ParentLogoImageTag) { + options.tag = item.ParentLogoImageTag; + return apiClient.getScaledImageUrl(item.ParentLogoItemId, options); + } + + return null; +} + +function renderLogo(page, item, apiClient) { + const detailLogo = page.querySelector('.detailLogo'); + + const url = logoImageUrl(item, apiClient, {}); + + if (!layoutManager.mobile && !userSettings.enableBackdrops()) { + detailLogo.classList.add('hide'); + } else if (url) { + detailLogo.classList.remove('hide'); + imageLoader.setLazyImage(detailLogo, url); + } else { + detailLogo.classList.add('hide'); + } +} + +function showRecordingFields(instance, page, item, user) { + if (!instance.currentRecordingFields) { + const recordingFieldsElement = page.querySelector('.recordingFields'); + + if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) { + import('recordingFields').then(({ default: recordingFields }) => { + instance.currentRecordingFields = new recordingFields({ + parent: recordingFieldsElement, + programId: item.Id, + serverId: item.ServerId }); - } else { - recordingFieldsElement.classList.add('hide'); - recordingFieldsElement.innerHTML = ''; - } - } - } - - function renderLinks(page, item) { - const externalLinksElem = page.querySelector('.itemExternalLinks'); - - const links = []; - - if (!layoutManager.tv && item.HomePageUrl) { - links.push(`${globalize.translate('ButtonWebsite')}`); - } - - if (item.ExternalUrls) { - for (const url of item.ExternalUrls) { - links.push(`${url.Name}`); - } - } - - const html = []; - if (links.length) { - html.push(links.join(', ')); - } - - externalLinksElem.innerHTML = html.join(', '); - - if (html.length) { - externalLinksElem.classList.remove('hide'); - } else { - externalLinksElem.classList.add('hide'); - } - } - - function renderDetailImage(elem, item, imageLoader) { - const itemArray = []; - itemArray.push(item); - const cardHtml = cardBuilder.getCardsHtml(itemArray, { - shape: 'auto', - showTitle: false, - centerText: true, - overlayText: false, - transition: false, - disableIndicators: true, - overlayPlayButton: true, - action: 'play', - width: dom.getWindowSize().innerWidth * 0.25 - }); - - elem.innerHTML = cardHtml; - imageLoader.lazyChildren(elem); - } - - function renderImage(page, item) { - renderDetailImage( - page.querySelector('.detailImageContainer'), - item, - imageLoader - ); - } - - function refreshDetailImageUserData(elem, item) { - elem.querySelector('.detailImageProgressContainer').innerHTML = indicators.getProgressBarHtml(item); - } - - function refreshImage(page, item) { - refreshDetailImageUserData(page.querySelector('.detailImageContainer'), item); - } - - function setPeopleHeader(page, item) { - if (item.MediaType == 'Audio' || item.Type == 'MusicAlbum' || item.MediaType == 'Book' || item.MediaType == 'Photo') { - page.querySelector('#peopleHeader').innerHTML = globalize.translate('People'); - } else { - page.querySelector('#peopleHeader').innerHTML = globalize.translate('HeaderCastAndCrew'); - } - } - - function renderNextUp(page, item, user) { - const section = page.querySelector('.nextUpSection'); - - if (item.Type != 'Series') { - return void section.classList.add('hide'); - } - - connectionManager.getApiClient(item.ServerId).getNextUpEpisodes({ - SeriesId: item.Id, - UserId: user.Id - }).then(function (result) { - if (result.Items.length) { - section.classList.remove('hide'); - } else { - section.classList.add('hide'); - } - - const html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'overflowBackdrop', - showTitle: true, - displayAsSpecial: item.Type == 'Season' && item.IndexNumber, - overlayText: false, - centerText: true, - overlayPlayButton: true + recordingFieldsElement.classList.remove('hide'); }); - const itemsContainer = section.querySelector('.nextUpItems'); - itemsContainer.innerHTML = html; - imageLoader.lazyChildren(itemsContainer); - }); + } else { + recordingFieldsElement.classList.add('hide'); + recordingFieldsElement.innerHTML = ''; + } + } +} + +function renderLinks(page, item) { + const externalLinksElem = page.querySelector('.itemExternalLinks'); + + const links = []; + + if (!layoutManager.tv && item.HomePageUrl) { + links.push(`${globalize.translate('ButtonWebsite')}`); } - function setInitialCollapsibleState(page, item, apiClient, context, user) { - page.querySelector('.collectionItems').innerHTML = ''; + if (item.ExternalUrls) { + for (const url of item.ExternalUrls) { + links.push(`${url.Name}`); + } + } - if (item.Type == 'Playlist') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - renderPlaylistItems(page, item); - } else if (item.Type == 'Studio' || item.Type == 'Person' || item.Type == 'Genre' || item.Type == 'MusicGenre' || item.Type == 'MusicArtist') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - renderItemsByName(page, item); - } else if (item.IsFolder) { - if (item.Type == 'BoxSet') { - page.querySelector('#childrenCollapsible').classList.add('hide'); - } + const html = []; + if (links.length) { + html.push(links.join(', ')); + } - renderChildren(page, item); + externalLinksElem.innerHTML = html.join(', '); + + if (html.length) { + externalLinksElem.classList.remove('hide'); + } else { + externalLinksElem.classList.add('hide'); + } +} + +function renderDetailImage(elem, item, imageLoader) { + const itemArray = []; + itemArray.push(item); + const cardHtml = cardBuilder.getCardsHtml(itemArray, { + shape: 'auto', + showTitle: false, + centerText: true, + overlayText: false, + transition: false, + disableIndicators: true, + overlayPlayButton: true, + action: 'play', + width: dom.getWindowSize().innerWidth * 0.25 + }); + + elem.innerHTML = cardHtml; + imageLoader.lazyChildren(elem); +} + +function renderImage(page, item) { + renderDetailImage( + page.querySelector('.detailImageContainer'), + item, + imageLoader + ); +} + +function refreshDetailImageUserData(elem, item) { + elem.querySelector('.detailImageProgressContainer').innerHTML = indicators.getProgressBarHtml(item); +} + +function refreshImage(page, item) { + refreshDetailImageUserData(page.querySelector('.detailImageContainer'), item); +} + +function setPeopleHeader(page, item) { + if (item.MediaType == 'Audio' || item.Type == 'MusicAlbum' || item.MediaType == 'Book' || item.MediaType == 'Photo') { + page.querySelector('#peopleHeader').innerHTML = globalize.translate('People'); + } else { + page.querySelector('#peopleHeader').innerHTML = globalize.translate('HeaderCastAndCrew'); + } +} + +function renderNextUp(page, item, user) { + const section = page.querySelector('.nextUpSection'); + + if (item.Type != 'Series') { + return void section.classList.add('hide'); + } + + connectionManager.getApiClient(item.ServerId).getNextUpEpisodes({ + SeriesId: item.Id, + UserId: user.Id + }).then(function (result) { + if (result.Items.length) { + section.classList.remove('hide'); } else { + section.classList.add('hide'); + } + + const html = cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'overflowBackdrop', + showTitle: true, + displayAsSpecial: item.Type == 'Season' && item.IndexNumber, + overlayText: false, + centerText: true, + overlayPlayButton: true + }); + const itemsContainer = section.querySelector('.nextUpItems'); + itemsContainer.innerHTML = html; + imageLoader.lazyChildren(itemsContainer); + }); +} + +function setInitialCollapsibleState(page, item, apiClient, context, user) { + page.querySelector('.collectionItems').innerHTML = ''; + + if (item.Type == 'Playlist') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + renderPlaylistItems(page, item); + } else if (item.Type == 'Studio' || item.Type == 'Person' || item.Type == 'Genre' || item.Type == 'MusicGenre' || item.Type == 'MusicArtist') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + renderItemsByName(page, item); + } else if (item.IsFolder) { + if (item.Type == 'BoxSet') { page.querySelector('#childrenCollapsible').classList.add('hide'); } - if (item.Type == 'Series') { - renderSeriesSchedule(page, item); - renderNextUp(page, item, user); - } else { - page.querySelector('.nextUpSection').classList.add('hide'); - } - - renderScenes(page, item); - - if (item.SpecialFeatureCount && item.SpecialFeatureCount != 0 && item.Type != 'Series') { - page.querySelector('#specialsCollapsible').classList.remove('hide'); - renderSpecials(page, item, user); - } else { - page.querySelector('#specialsCollapsible').classList.add('hide'); - } - - renderCast(page, item); - - if (item.PartCount && item.PartCount > 1) { - page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); - renderAdditionalParts(page, item, user); - } else { - page.querySelector('#additionalPartsCollapsible').classList.add('hide'); - } - - if (item.Type == 'MusicAlbum') { - renderMusicVideos(page, item, user); - } else { - page.querySelector('#musicVideosCollapsible').classList.add('hide'); - } + renderChildren(page, item); + } else { + page.querySelector('#childrenCollapsible').classList.add('hide'); } - function toggleLineClamp(clampTarget, e) { - const expandButton = e.target; - const clampClassName = 'detail-clamp-text'; - - if (clampTarget.classList.contains(clampClassName)) { - clampTarget.classList.remove(clampClassName); - expandButton.innerHTML = globalize.translate('ShowLess'); - } else { - clampTarget.classList.add(clampClassName); - expandButton.innerHTML = globalize.translate('ShowMore'); - } + if (item.Type == 'Series') { + renderSeriesSchedule(page, item); + renderNextUp(page, item, user); + } else { + page.querySelector('.nextUpSection').classList.add('hide'); } - function renderOverview(page, item) { - for (const overviewElemnt of page.querySelectorAll('.overview')) { - const overview = item.Overview || ''; + renderScenes(page, item); - if (overview) { - overviewElemnt.innerHTML = overview; - overviewElemnt.classList.remove('hide'); - overviewElemnt.classList.add('detail-clamp-text'); + if (item.SpecialFeatureCount && item.SpecialFeatureCount != 0 && item.Type != 'Series') { + page.querySelector('#specialsCollapsible').classList.remove('hide'); + renderSpecials(page, item, user); + } else { + page.querySelector('#specialsCollapsible').classList.add('hide'); + } - // Grab the sibling element to control the expand state - const expandButton = overviewElemnt.parentElement.querySelector('.overview-expand'); + renderCast(page, item); - // Detect if we have overflow of text. Based on this StackOverflow answer - // https://stackoverflow.com/a/35157976 - if (Math.abs(overviewElemnt.scrollHeight - overviewElemnt.offsetHeight) > 2) { - expandButton.classList.remove('hide'); - } else { - expandButton.classList.add('hide'); - } + if (item.PartCount && item.PartCount > 1) { + page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); + renderAdditionalParts(page, item, user); + } else { + page.querySelector('#additionalPartsCollapsible').classList.add('hide'); + } - expandButton.addEventListener('click', toggleLineClamp.bind(null, overviewElemnt)); + if (item.Type == 'MusicAlbum') { + renderMusicVideos(page, item, user); + } else { + page.querySelector('#musicVideosCollapsible').classList.add('hide'); + } +} - for (const anchor of overviewElemnt.querySelectorAll('a')) { - anchor.setAttribute('target', '_blank'); - } +function toggleLineClamp(clampTarget, e) { + const expandButton = e.target; + const clampClassName = 'detail-clamp-text'; + + if (clampTarget.classList.contains(clampClassName)) { + clampTarget.classList.remove(clampClassName); + expandButton.innerHTML = globalize.translate('ShowLess'); + } else { + clampTarget.classList.add(clampClassName); + expandButton.innerHTML = globalize.translate('ShowMore'); + } +} + +function renderOverview(page, item) { + for (const overviewElemnt of page.querySelectorAll('.overview')) { + const overview = item.Overview || ''; + + if (overview) { + overviewElemnt.innerHTML = overview; + overviewElemnt.classList.remove('hide'); + overviewElemnt.classList.add('detail-clamp-text'); + + // Grab the sibling element to control the expand state + const expandButton = overviewElemnt.parentElement.querySelector('.overview-expand'); + + // Detect if we have overflow of text. Based on this StackOverflow answer + // https://stackoverflow.com/a/35157976 + if (Math.abs(overviewElemnt.scrollHeight - overviewElemnt.offsetHeight) > 2) { + expandButton.classList.remove('hide'); } else { - overviewElemnt.innerHTML = ''; - overviewElemnt.classList.add('hide'); + expandButton.classList.add('hide'); } - } - } - function renderGenres(page, item, context = inferContext(item)) { - const genres = item.GenreItems || []; - const type = context === 'music' ? 'MusicGenre' : 'Genre'; + expandButton.addEventListener('click', toggleLineClamp.bind(null, overviewElemnt)); - const html = genres.map(function (p) { - return '' + p.Name + ''; - }).join(', '); - - const genresLabel = page.querySelector('.genresLabel'); - genresLabel.innerHTML = globalize.translate(genres.length > 1 ? 'Genres' : 'Genre'); - const genresValue = page.querySelector('.genres'); - genresValue.innerHTML = html; - - const genresGroup = page.querySelector('.genresGroup'); - if (genres.length) { - genresGroup.classList.remove('hide'); + for (const anchor of overviewElemnt.querySelectorAll('a')) { + anchor.setAttribute('target', '_blank'); + } } else { - genresGroup.classList.add('hide'); + overviewElemnt.innerHTML = ''; + overviewElemnt.classList.add('hide'); } } +} - function renderWriter(page, item, context) { - const writers = (item.People || []).filter(function (person) { - return person.Type === 'Writer'; +function renderGenres(page, item, context = inferContext(item)) { + const genres = item.GenreItems || []; + const type = context === 'music' ? 'MusicGenre' : 'Genre'; + + const html = genres.map(function (p) { + return '' + p.Name + ''; + }).join(', '); + + const genresLabel = page.querySelector('.genresLabel'); + genresLabel.innerHTML = globalize.translate(genres.length > 1 ? 'Genres' : 'Genre'); + const genresValue = page.querySelector('.genres'); + genresValue.innerHTML = html; + + const genresGroup = page.querySelector('.genresGroup'); + if (genres.length) { + genresGroup.classList.remove('hide'); + } else { + genresGroup.classList.add('hide'); + } +} + +function renderWriter(page, item, context) { + const writers = (item.People || []).filter(function (person) { + return person.Type === 'Writer'; + }); + + const html = writers.map(function (person) { + return '' + person.Name + ''; + }).join(', '); + + const writersLabel = page.querySelector('.writersLabel'); + writersLabel.innerHTML = globalize.translate(writers.length > 1 ? 'Writers' : 'Writer'); + const writersValue = page.querySelector('.writers'); + writersValue.innerHTML = html; + + const writersGroup = page.querySelector('.writersGroup'); + if (writers.length) { + writersGroup.classList.remove('hide'); + } else { + writersGroup.classList.add('hide'); + } +} + +function renderDirector(page, item, context) { + const directors = (item.People || []).filter(function (person) { + return person.Type === 'Director'; + }); + + const html = directors.map(function (person) { + return '' + person.Name + ''; + }).join(', '); + + const directorsLabel = page.querySelector('.directorsLabel'); + directorsLabel.innerHTML = globalize.translate(directors.length > 1 ? 'Directors' : 'Director'); + const directorsValue = page.querySelector('.directors'); + directorsValue.innerHTML = html; + + const directorsGroup = page.querySelector('.directorsGroup'); + if (directors.length) { + directorsGroup.classList.remove('hide'); + } else { + directorsGroup.classList.add('hide'); + } +} + +function renderMiscInfo(page, item) { + const primaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-primary'); + + for (const miscInfo of primaryItemMiscInfo) { + mediaInfo.fillPrimaryMediaInfo(miscInfo, item, { + interactive: true, + episodeTitle: false, + subtitles: false }); - const html = writers.map(function (person) { - return '' + person.Name + ''; - }).join(', '); - - const writersLabel = page.querySelector('.writersLabel'); - writersLabel.innerHTML = globalize.translate(writers.length > 1 ? 'Writers' : 'Writer'); - const writersValue = page.querySelector('.writers'); - writersValue.innerHTML = html; - - const writersGroup = page.querySelector('.writersGroup'); - if (writers.length) { - writersGroup.classList.remove('hide'); + if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { + miscInfo.classList.remove('hide'); } else { - writersGroup.classList.add('hide'); + miscInfo.classList.add('hide'); } } - function renderDirector(page, item, context) { - const directors = (item.People || []).filter(function (person) { - return person.Type === 'Director'; + const secondaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-secondary'); + + for (const miscInfo of secondaryItemMiscInfo) { + mediaInfo.fillSecondaryMediaInfo(miscInfo, item, { + interactive: true }); - const html = directors.map(function (person) { - return '' + person.Name + ''; - }).join(', '); - - const directorsLabel = page.querySelector('.directorsLabel'); - directorsLabel.innerHTML = globalize.translate(directors.length > 1 ? 'Directors' : 'Director'); - const directorsValue = page.querySelector('.directors'); - directorsValue.innerHTML = html; - - const directorsGroup = page.querySelector('.directorsGroup'); - if (directors.length) { - directorsGroup.classList.remove('hide'); + if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { + miscInfo.classList.remove('hide'); } else { - directorsGroup.classList.add('hide'); + miscInfo.classList.add('hide'); } } +} - function renderMiscInfo(page, item) { - const primaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-primary'); +function renderTagline(page, item) { + const taglineElement = page.querySelector('.tagline'); - for (const miscInfo of primaryItemMiscInfo) { - mediaInfo.fillPrimaryMediaInfo(miscInfo, item, { - interactive: true, - episodeTitle: false, - subtitles: false - }); + if (item.Taglines && item.Taglines.length) { + taglineElement.classList.remove('hide'); + taglineElement.innerHTML = item.Taglines[0]; + } else { + taglineElement.classList.add('hide'); + } +} - if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { - miscInfo.classList.remove('hide'); - } else { - miscInfo.classList.add('hide'); - } +function renderDetails(page, item, apiClient, context, isStatic) { + renderSimilarItems(page, item, context); + renderMoreFromSeason(page, item, apiClient); + renderMoreFromArtist(page, item, apiClient); + renderDirector(page, item, context); + renderWriter(page, item, context); + renderGenres(page, item, context); + renderChannelGuide(page, apiClient, item); + renderTagline(page, item); + renderOverview(page, item); + renderMiscInfo(page, item); + reloadUserDataButtons(page, item); + renderLinks(page, item); + renderTags(page, item); + renderSeriesAirTime(page, item, isStatic); +} + +function enableScrollX() { + return browser.mobile && screen.availWidth <= 1000; +} + +function getPortraitShape(scrollX) { + if (scrollX == null) { + scrollX = enableScrollX(); + } + + return scrollX ? 'overflowPortrait' : 'portrait'; +} + +function getSquareShape(scrollX) { + if (scrollX == null) { + scrollX = enableScrollX(); + } + + return scrollX ? 'overflowSquare' : 'square'; +} + +function renderMoreFromSeason(view, item, apiClient) { + const section = view.querySelector('.moreFromSeasonSection'); + + if (section) { + if (item.Type !== 'Episode' || !item.SeasonId || !item.SeriesId) { + return void section.classList.add('hide'); } - const secondaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-secondary'); - - for (const miscInfo of secondaryItemMiscInfo) { - mediaInfo.fillSecondaryMediaInfo(miscInfo, item, { - interactive: true - }); - - if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { - miscInfo.classList.remove('hide'); - } else { - miscInfo.classList.add('hide'); - } - } - } - - function renderTagline(page, item) { - const taglineElement = page.querySelector('.tagline'); - - if (item.Taglines && item.Taglines.length) { - taglineElement.classList.remove('hide'); - taglineElement.innerHTML = item.Taglines[0]; - } else { - taglineElement.classList.add('hide'); - } - } - - function renderDetails(page, item, apiClient, context, isStatic) { - renderSimilarItems(page, item, context); - renderMoreFromSeason(page, item, apiClient); - renderMoreFromArtist(page, item, apiClient); - renderDirector(page, item, context); - renderWriter(page, item, context); - renderGenres(page, item, context); - renderChannelGuide(page, apiClient, item); - renderTagline(page, item); - renderOverview(page, item); - renderMiscInfo(page, item); - reloadUserDataButtons(page, item); - renderLinks(page, item); - renderTags(page, item); - renderSeriesAirTime(page, item, isStatic); - } - - function enableScrollX() { - return browser.mobile && screen.availWidth <= 1000; - } - - function getPortraitShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowPortrait' : 'portrait'; - } - - function getSquareShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowSquare' : 'square'; - } - - function renderMoreFromSeason(view, item, apiClient) { - const section = view.querySelector('.moreFromSeasonSection'); - - if (section) { - if (item.Type !== 'Episode' || !item.SeasonId || !item.SeriesId) { + const userId = apiClient.getCurrentUserId(); + apiClient.getEpisodes(item.SeriesId, { + SeasonId: item.SeasonId, + UserId: userId, + Fields: 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount' + }).then(function (result) { + if (result.Items.length < 2) { return void section.classList.add('hide'); } - const userId = apiClient.getCurrentUserId(); - apiClient.getEpisodes(item.SeriesId, { - SeasonId: item.SeasonId, - UserId: userId, - Fields: 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount' - }).then(function (result) { - if (result.Items.length < 2) { - return void section.classList.add('hide'); - } - - section.classList.remove('hide'); - section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.SeasonName); - const itemsContainer = section.querySelector('.itemsContainer'); - cardBuilder.buildCards(result.Items, { - parentContainer: section, - itemsContainer: itemsContainer, - shape: 'autooverflow', - sectionTitleTagName: 'h2', - scalable: true, - showTitle: true, - overlayText: false, - centerText: true, - includeParentInfoInTitle: false, - allowBottomPadding: false - }); - const card = itemsContainer.querySelector('.card[data-id="' + item.Id + '"]'); - - if (card) { - setTimeout(function () { - section.querySelector('.emby-scroller').toStart(card.previousSibling || card, true); - }, 100); - } + section.classList.remove('hide'); + section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.SeasonName); + const itemsContainer = section.querySelector('.itemsContainer'); + cardBuilder.buildCards(result.Items, { + parentContainer: section, + itemsContainer: itemsContainer, + shape: 'autooverflow', + sectionTitleTagName: 'h2', + scalable: true, + showTitle: true, + overlayText: false, + centerText: true, + includeParentInfoInTitle: false, + allowBottomPadding: false }); - } + const card = itemsContainer.querySelector('.card[data-id="' + item.Id + '"]'); + + if (card) { + setTimeout(function () { + section.querySelector('.emby-scroller').toStart(card.previousSibling || card, true); + }, 100); + } + }); } +} - function renderMoreFromArtist(view, item, apiClient) { - const section = view.querySelector('.moreFromArtistSection'); +function renderMoreFromArtist(view, item, apiClient) { + const section = view.querySelector('.moreFromArtistSection'); - if (section) { - if (item.Type === 'MusicArtist') { - if (!apiClient.isMinServerVersion('3.4.1.19')) { - return void section.classList.add('hide'); - } - } else if (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length) { + if (section) { + if (item.Type === 'MusicArtist') { + if (!apiClient.isMinServerVersion('3.4.1.19')) { + return void section.classList.add('hide'); + } + } else if (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length) { + return void section.classList.add('hide'); + } + + const query = { + IncludeItemTypes: 'MusicAlbum', + Recursive: true, + ExcludeItemIds: item.Id, + SortBy: 'ProductionYear,SortName', + SortOrder: 'Descending' + }; + + if (item.Type === 'MusicArtist') { + query.ContributingArtistIds = item.Id; + } else if (apiClient.isMinServerVersion('3.4.1.18')) { + query.AlbumArtistIds = item.AlbumArtists[0].Id; + } else { + query.ArtistIds = item.AlbumArtists[0].Id; + } + + apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) { + if (!result.Items.length) { return void section.classList.add('hide'); } - const query = { - IncludeItemTypes: 'MusicAlbum', - Recursive: true, - ExcludeItemIds: item.Id, - SortBy: 'ProductionYear,SortName', - SortOrder: 'Descending' - }; + section.classList.remove('hide'); if (item.Type === 'MusicArtist') { - query.ContributingArtistIds = item.Id; - } else if (apiClient.isMinServerVersion('3.4.1.18')) { - query.AlbumArtistIds = item.AlbumArtists[0].Id; + section.querySelector('h2').innerHTML = globalize.translate('HeaderAppearsOn'); } else { - query.ArtistIds = item.AlbumArtists[0].Id; + section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.AlbumArtists[0].Name); } - apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) { - if (!result.Items.length) { - return void section.classList.add('hide'); - } - - section.classList.remove('hide'); - - if (item.Type === 'MusicArtist') { - section.querySelector('h2').innerHTML = globalize.translate('HeaderAppearsOn'); - } else { - section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.AlbumArtists[0].Name); - } - - cardBuilder.buildCards(result.Items, { - parentContainer: section, - itemsContainer: section.querySelector('.itemsContainer'), - shape: 'autooverflow', - sectionTitleTagName: 'h2', - scalable: true, - coverImage: item.Type === 'MusicArtist' || item.Type === 'MusicAlbum', - showTitle: true, - showParentTitle: false, - centerText: true, - overlayText: false, - overlayPlayButton: true, - showYear: true - }); + cardBuilder.buildCards(result.Items, { + parentContainer: section, + itemsContainer: section.querySelector('.itemsContainer'), + shape: 'autooverflow', + sectionTitleTagName: 'h2', + scalable: true, + coverImage: item.Type === 'MusicArtist' || item.Type === 'MusicAlbum', + showTitle: true, + showParentTitle: false, + centerText: true, + overlayText: false, + overlayPlayButton: true, + showYear: true }); - } + }); } +} - function renderSimilarItems(page, item, context) { - const similarCollapsible = page.querySelector('#similarCollapsible'); +function renderSimilarItems(page, item, context) { + const similarCollapsible = page.querySelector('#similarCollapsible'); - if (similarCollapsible) { - if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') { + if (similarCollapsible) { + if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') { + return void similarCollapsible.classList.add('hide'); + } + + similarCollapsible.classList.remove('hide'); + const apiClient = connectionManager.getApiClient(item.ServerId); + const options = { + userId: apiClient.getCurrentUserId(), + limit: 12, + fields: 'PrimaryImageAspectRatio,UserData,CanDelete' + }; + + if (item.Type == 'MusicAlbum' && item.AlbumArtists && item.AlbumArtists.length) { + options.ExcludeArtistIds = item.AlbumArtists[0].Id; + } + + apiClient.getSimilarItems(item.Id, options).then(function (result) { + if (!result.Items.length) { return void similarCollapsible.classList.add('hide'); } similarCollapsible.classList.remove('hide'); - const apiClient = connectionManager.getApiClient(item.ServerId); - const options = { - userId: apiClient.getCurrentUserId(), - limit: 12, - fields: 'PrimaryImageAspectRatio,UserData,CanDelete' - }; - - if (item.Type == 'MusicAlbum' && item.AlbumArtists && item.AlbumArtists.length) { - options.ExcludeArtistIds = item.AlbumArtists[0].Id; - } - - apiClient.getSimilarItems(item.Id, options).then(function (result) { - if (!result.Items.length) { - return void similarCollapsible.classList.add('hide'); - } - - similarCollapsible.classList.remove('hide'); - let html = ''; - html += cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'autooverflow', - showParentTitle: item.Type == 'MusicAlbum', - centerText: true, - showTitle: true, - context: context, - lazy: true, - showDetailsMenu: true, - coverImage: item.Type == 'MusicAlbum' || item.Type == 'MusicArtist', - overlayPlayButton: true, - overlayText: false, - showYear: item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Series' - }); - const similarContent = similarCollapsible.querySelector('.similarContent'); - similarContent.innerHTML = html; - imageLoader.lazyChildren(similarContent); - }); - } - } - - function renderSeriesAirTime(page, item, isStatic) { - const seriesAirTime = page.querySelector('#seriesAirTime'); - if (item.Type != 'Series') { - seriesAirTime.classList.add('hide'); - return; - } - let html = ''; - if (item.AirDays && item.AirDays.length) { - if (item.AirDays.length == 7) { - html += 'daily'; - } else { - html += item.AirDays.map(function (a) { - return a + 's'; - }).join(','); - } - } - if (item.AirTime) { - html += ' at ' + item.AirTime; - } - if (item.Studios.length) { - if (isStatic) { - html += ' on ' + item.Studios[0].Name; - } else { - const context = inferContext(item); - const href = appRouter.getRouteUrl(item.Studios[0], { - context: context, - itemType: 'Studio', - serverId: item.ServerId - }); - html += ' on ' + item.Studios[0].Name + ''; - } - } - if (html) { - html = (item.Status == 'Ended' ? 'Aired ' : 'Airs ') + html; - seriesAirTime.innerHTML = html; - seriesAirTime.classList.remove('hide'); - } else { - seriesAirTime.classList.add('hide'); - } - } - - function renderTags(page, item) { - const itemTags = page.querySelector('.itemTags'); - const tagElements = []; - let tags = item.Tags || []; - - if (item.Type === 'Program') { - tags = []; - } - - for (let i = 0, length = tags.length; i < length; i++) { - tagElements.push(tags[i]); - } - - if (tagElements.length) { - itemTags.innerHTML = globalize.translate('TagsValue', tagElements.join(', ')); - itemTags.classList.remove('hide'); - } else { - itemTags.innerHTML = ''; - itemTags.classList.add('hide'); - } - } - - function renderChildren(page, item) { - let fields = 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount'; - const query = { - ParentId: item.Id, - Fields: fields - }; - - if (item.Type !== 'BoxSet') { - query.SortBy = 'SortName'; - } - - let promise; - const apiClient = connectionManager.getApiClient(item.ServerId); - const userId = apiClient.getCurrentUserId(); - - if (item.Type == 'Series') { - promise = apiClient.getSeasons(item.Id, { - userId: userId, - Fields: fields - }); - } else if (item.Type == 'Season') { - fields += ',Overview'; - promise = apiClient.getEpisodes(item.SeriesId, { - seasonId: item.Id, - userId: userId, - Fields: fields - }); - } else if (item.Type == 'MusicArtist') { - query.SortBy = 'ProductionYear,SortName'; - } - - promise = promise || apiClient.getItems(apiClient.getCurrentUserId(), query); - promise.then(function (result) { let html = ''; - let scrollX = false; - let isList = false; - const childrenItemsContainer = page.querySelector('.childrenItemsContainer'); + html += cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'autooverflow', + showParentTitle: item.Type == 'MusicAlbum', + centerText: true, + showTitle: true, + context: context, + lazy: true, + showDetailsMenu: true, + coverImage: item.Type == 'MusicAlbum' || item.Type == 'MusicArtist', + overlayPlayButton: true, + overlayText: false, + showYear: item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Series' + }); + const similarContent = similarCollapsible.querySelector('.similarContent'); + similarContent.innerHTML = html; + imageLoader.lazyChildren(similarContent); + }); + } +} - if (item.Type == 'MusicAlbum') { - html = listView.getListViewHtml({ - items: result.Items, - smallIcon: true, - showIndex: true, - index: 'disc', - showIndexNumberLeft: true, - playFromHere: true, - action: 'playallfromhere', - image: false, - artist: 'auto', - containerAlbumArtists: item.AlbumArtists - }); +function renderSeriesAirTime(page, item, isStatic) { + const seriesAirTime = page.querySelector('#seriesAirTime'); + if (item.Type != 'Series') { + seriesAirTime.classList.add('hide'); + return; + } + let html = ''; + if (item.AirDays && item.AirDays.length) { + if (item.AirDays.length == 7) { + html += 'daily'; + } else { + html += item.AirDays.map(function (a) { + return a + 's'; + }).join(','); + } + } + if (item.AirTime) { + html += ' at ' + item.AirTime; + } + if (item.Studios.length) { + if (isStatic) { + html += ' on ' + item.Studios[0].Name; + } else { + const context = inferContext(item); + const href = appRouter.getRouteUrl(item.Studios[0], { + context: context, + itemType: 'Studio', + serverId: item.ServerId + }); + html += ' on ' + item.Studios[0].Name + ''; + } + } + if (html) { + html = (item.Status == 'Ended' ? 'Aired ' : 'Airs ') + html; + seriesAirTime.innerHTML = html; + seriesAirTime.classList.remove('hide'); + } else { + seriesAirTime.classList.add('hide'); + } +} + +function renderTags(page, item) { + const itemTags = page.querySelector('.itemTags'); + const tagElements = []; + let tags = item.Tags || []; + + if (item.Type === 'Program') { + tags = []; + } + + for (let i = 0, length = tags.length; i < length; i++) { + tagElements.push(tags[i]); + } + + if (tagElements.length) { + itemTags.innerHTML = globalize.translate('TagsValue', tagElements.join(', ')); + itemTags.classList.remove('hide'); + } else { + itemTags.innerHTML = ''; + itemTags.classList.add('hide'); + } +} + +function renderChildren(page, item) { + let fields = 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount'; + const query = { + ParentId: item.Id, + Fields: fields + }; + + if (item.Type !== 'BoxSet') { + query.SortBy = 'SortName'; + } + + let promise; + const apiClient = connectionManager.getApiClient(item.ServerId); + const userId = apiClient.getCurrentUserId(); + + if (item.Type == 'Series') { + promise = apiClient.getSeasons(item.Id, { + userId: userId, + Fields: fields + }); + } else if (item.Type == 'Season') { + fields += ',Overview'; + promise = apiClient.getEpisodes(item.SeriesId, { + seasonId: item.Id, + userId: userId, + Fields: fields + }); + } else if (item.Type == 'MusicArtist') { + query.SortBy = 'ProductionYear,SortName'; + } + + promise = promise || apiClient.getItems(apiClient.getCurrentUserId(), query); + promise.then(function (result) { + let html = ''; + let scrollX = false; + let isList = false; + const childrenItemsContainer = page.querySelector('.childrenItemsContainer'); + + if (item.Type == 'MusicAlbum') { + html = listView.getListViewHtml({ + items: result.Items, + smallIcon: true, + showIndex: true, + index: 'disc', + showIndexNumberLeft: true, + playFromHere: true, + action: 'playallfromhere', + image: false, + artist: 'auto', + containerAlbumArtists: item.AlbumArtists + }); + isList = true; + } else if (item.Type == 'Series') { + scrollX = enableScrollX(); + html = cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'overflowPortrait', + showTitle: true, + centerText: true, + lazy: true, + overlayPlayButton: true, + allowBottomPadding: !scrollX + }); + } else if (item.Type == 'Season' || item.Type == 'Episode') { + if (item.Type !== 'Episode') { isList = true; - } else if (item.Type == 'Series') { - scrollX = enableScrollX(); + } + scrollX = item.Type == 'Episode'; + if (result.Items.length < 2 && item.Type === 'Episode') { + return; + } + + if (item.Type === 'Episode') { html = cardBuilder.getCardsHtml({ items: result.Items, - shape: 'overflowPortrait', + shape: 'overflowBackdrop', showTitle: true, - centerText: true, + displayAsSpecial: item.Type == 'Season' && item.IndexNumber, + playFromHere: true, + overlayText: true, lazy: true, + showDetailsMenu: true, overlayPlayButton: true, - allowBottomPadding: !scrollX + allowBottomPadding: !scrollX, + includeParentInfoInTitle: false + }); + } else if (item.Type === 'Season') { + html = listView.getListViewHtml({ + items: result.Items, + showIndexNumber: false, + enableOverview: true, + enablePlayedButton: layoutManager.mobile ? false : true, + infoButton: layoutManager.mobile ? false : true, + imageSize: 'large', + enableSideMediaInfo: false, + highlight: false, + action: !layoutManager.desktop ? 'link' : 'none', + imagePlayButton: true, + includeParentInfoInTitle: false }); - } else if (item.Type == 'Season' || item.Type == 'Episode') { - if (item.Type !== 'Episode') { - isList = true; - } - scrollX = item.Type == 'Episode'; - if (result.Items.length < 2 && item.Type === 'Episode') { - return; - } - - if (item.Type === 'Episode') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'overflowBackdrop', - showTitle: true, - displayAsSpecial: item.Type == 'Season' && item.IndexNumber, - playFromHere: true, - overlayText: true, - lazy: true, - showDetailsMenu: true, - overlayPlayButton: true, - allowBottomPadding: !scrollX, - includeParentInfoInTitle: false - }); - } else if (item.Type === 'Season') { - html = listView.getListViewHtml({ - items: result.Items, - showIndexNumber: false, - enableOverview: true, - enablePlayedButton: layoutManager.mobile ? false : true, - infoButton: layoutManager.mobile ? false : true, - imageSize: 'large', - enableSideMediaInfo: false, - highlight: false, - action: !layoutManager.desktop ? 'link' : 'none', - imagePlayButton: true, - includeParentInfoInTitle: false - }); - } } + } - if (item.Type !== 'BoxSet') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - } - if (scrollX) { - childrenItemsContainer.classList.add('scrollX'); - childrenItemsContainer.classList.add('hiddenScrollX'); + if (item.Type !== 'BoxSet') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + } + if (scrollX) { + childrenItemsContainer.classList.add('scrollX'); + childrenItemsContainer.classList.add('hiddenScrollX'); + childrenItemsContainer.classList.remove('vertical-wrap'); + childrenItemsContainer.classList.remove('vertical-list'); + } else { + childrenItemsContainer.classList.remove('scrollX'); + childrenItemsContainer.classList.remove('hiddenScrollX'); + childrenItemsContainer.classList.remove('smoothScrollX'); + if (isList) { + childrenItemsContainer.classList.add('vertical-list'); childrenItemsContainer.classList.remove('vertical-wrap'); - childrenItemsContainer.classList.remove('vertical-list'); } else { - childrenItemsContainer.classList.remove('scrollX'); - childrenItemsContainer.classList.remove('hiddenScrollX'); - childrenItemsContainer.classList.remove('smoothScrollX'); - if (isList) { - childrenItemsContainer.classList.add('vertical-list'); - childrenItemsContainer.classList.remove('vertical-wrap'); - } else { - childrenItemsContainer.classList.add('vertical-wrap'); - childrenItemsContainer.classList.remove('vertical-list'); - } + childrenItemsContainer.classList.add('vertical-wrap'); + childrenItemsContainer.classList.remove('vertical-list'); } - if (layoutManager.mobile) { - childrenItemsContainer.classList.remove('padded-right'); - } - childrenItemsContainer.innerHTML = html; - imageLoader.lazyChildren(childrenItemsContainer); - if (item.Type == 'BoxSet') { - const collectionItemTypes = [{ - name: globalize.translate('HeaderVideos'), - mediaType: 'Video' - }, { - name: globalize.translate('Series'), - type: 'Series' - }, { - name: globalize.translate('Albums'), - type: 'MusicAlbum' - }, { - name: globalize.translate('Books'), - type: 'Book' - }]; - renderCollectionItems(page, item, collectionItemTypes, result.Items); - } - }); - - if (item.Type == 'Season') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('Episodes'); - } else if (item.Type == 'Series') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderSeasons'); - } else if (item.Type == 'MusicAlbum') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderTracks'); - } else { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('Items'); } - - if (item.Type == 'MusicAlbum' || item.Type == 'Season') { - page.querySelector('.childrenSectionHeader').classList.add('hide'); - page.querySelector('#childrenCollapsible').classList.add('verticalSection-extrabottompadding'); - } else { - page.querySelector('.childrenSectionHeader').classList.remove('hide'); + if (layoutManager.mobile) { + childrenItemsContainer.classList.remove('padded-right'); } + childrenItemsContainer.innerHTML = html; + imageLoader.lazyChildren(childrenItemsContainer); + if (item.Type == 'BoxSet') { + const collectionItemTypes = [{ + name: globalize.translate('HeaderVideos'), + mediaType: 'Video' + }, { + name: globalize.translate('Series'), + type: 'Series' + }, { + name: globalize.translate('Albums'), + type: 'MusicAlbum' + }, { + name: globalize.translate('Books'), + type: 'Book' + }]; + renderCollectionItems(page, item, collectionItemTypes, result.Items); + } + }); + + if (item.Type == 'Season') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('Episodes'); + } else if (item.Type == 'Series') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderSeasons'); + } else if (item.Type == 'MusicAlbum') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderTracks'); + } else { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('Items'); } - function renderItemsByName(page, item) { - import('scripts/itembynamedetailpage').then(() => { - window.ItemsByName.renderItems(page, item); - }); + if (item.Type == 'MusicAlbum' || item.Type == 'Season') { + page.querySelector('.childrenSectionHeader').classList.add('hide'); + page.querySelector('#childrenCollapsible').classList.add('verticalSection-extrabottompadding'); + } else { + page.querySelector('.childrenSectionHeader').classList.remove('hide'); } +} - function renderPlaylistItems(page, item) { - import('scripts/playlistedit').then(() => { - PlaylistViewer.render(page, item); - }); - } +function renderItemsByName(page, item) { + import('scripts/itembynamedetailpage').then(() => { + window.ItemsByName.renderItems(page, item); + }); +} - function renderProgramsForChannel(page, result) { - let html = ''; - let currentItems = []; - let currentStartDate = null; +function renderPlaylistItems(page, item) { + import('scripts/playlistedit').then(() => { + PlaylistViewer.render(page, item); + }); +} - for (let i = 0, length = result.Items.length; i < length; i++) { - const item = result.Items[i]; - const itemStartDate = datetime.parseISO8601Date(item.StartDate); +function renderProgramsForChannel(page, result) { + let html = ''; + let currentItems = []; + let currentStartDate = null; - if (!(currentStartDate && currentStartDate.toDateString() === itemStartDate.toDateString())) { - if (currentItems.length) { - html += '
'; - html += '

' + datetime.toLocaleDateString(currentStartDate, { - weekday: 'long', - month: 'long', - day: 'numeric' - }) + '

'; - html += '
' + listView.getListViewHtml({ - items: currentItems, - enableUserDataButtons: false, - showParentTitle: true, - image: false, - showProgramTime: true, - mediaInfo: false, - parentTitleWithTitle: true - }) + '
'; - } + for (let i = 0, length = result.Items.length; i < length; i++) { + const item = result.Items[i]; + const itemStartDate = datetime.parseISO8601Date(item.StartDate); - currentStartDate = itemStartDate; - currentItems = []; + if (!(currentStartDate && currentStartDate.toDateString() === itemStartDate.toDateString())) { + if (currentItems.length) { + html += '
'; + html += '

' + datetime.toLocaleDateString(currentStartDate, { + weekday: 'long', + month: 'long', + day: 'numeric' + }) + '

'; + html += '
' + listView.getListViewHtml({ + items: currentItems, + enableUserDataButtons: false, + showParentTitle: true, + image: false, + showProgramTime: true, + mediaInfo: false, + parentTitleWithTitle: true + }) + '
'; } - currentItems.push(item); + currentStartDate = itemStartDate; + currentItems = []; } - if (currentItems.length) { - html += '
'; - html += '

' + datetime.toLocaleDateString(currentStartDate, { - weekday: 'long', - month: 'long', - day: 'numeric' - }) + '

'; - html += '
' + listView.getListViewHtml({ - items: currentItems, - enableUserDataButtons: false, - showParentTitle: true, - image: false, - showProgramTime: true, - mediaInfo: false, - parentTitleWithTitle: true - }) + '
'; - } - - page.querySelector('.programGuide').innerHTML = html; + currentItems.push(item); } - function renderChannelGuide(page, apiClient, item) { - if (item.Type === 'TvChannel') { - page.querySelector('.programGuideSection').classList.remove('hide'); - apiClient.getLiveTvPrograms({ - ChannelIds: item.Id, - UserId: apiClient.getCurrentUserId(), - HasAired: false, - SortBy: 'StartDate', - EnableTotalRecordCount: false, - EnableImages: false, - ImageTypeLimit: 0, - EnableUserData: false - }).then(function (result) { - renderProgramsForChannel(page, result); - }); - } + if (currentItems.length) { + html += '
'; + html += '

' + datetime.toLocaleDateString(currentStartDate, { + weekday: 'long', + month: 'long', + day: 'numeric' + }) + '

'; + html += '
' + listView.getListViewHtml({ + items: currentItems, + enableUserDataButtons: false, + showParentTitle: true, + image: false, + showProgramTime: true, + mediaInfo: false, + parentTitleWithTitle: true + }) + '
'; } - function renderSeriesSchedule(page, item) { - const apiClient = connectionManager.getApiClient(item.ServerId); + page.querySelector('.programGuide').innerHTML = html; +} + +function renderChannelGuide(page, apiClient, item) { + if (item.Type === 'TvChannel') { + page.querySelector('.programGuideSection').classList.remove('hide'); apiClient.getLiveTvPrograms({ + ChannelIds: item.Id, UserId: apiClient.getCurrentUserId(), HasAired: false, SortBy: 'StartDate', EnableTotalRecordCount: false, EnableImages: false, ImageTypeLimit: 0, - Limit: 50, - EnableUserData: false, - LibrarySeriesId: item.Id + EnableUserData: false }).then(function (result) { - if (result.Items.length) { - page.querySelector('#seriesScheduleSection').classList.remove('hide'); - } else { - page.querySelector('#seriesScheduleSection').classList.add('hide'); - } - - page.querySelector('#seriesScheduleList').innerHTML = listView.getListViewHtml({ - items: result.Items, - enableUserDataButtons: false, - showParentTitle: false, - image: false, - showProgramDateTime: true, - mediaInfo: false, - showTitle: true, - moreButton: false, - action: 'programdialog' - }); - loading.hide(); + renderProgramsForChannel(page, result); }); } +} - function inferContext(item) { - if (item.Type === 'Movie' || item.Type === 'BoxSet') { - return 'movies'; - } - - if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') { - return 'tvshows'; - } - - if (item.Type === 'MusicArtist' || item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'AudioBook') { - return 'music'; - } - - if (item.Type === 'Program') { - return 'livetv'; - } - - return null; - } - - function filterItemsByCollectionItemType(items, typeInfo) { - return items.filter(function (item) { - if (typeInfo.mediaType) { - return item.MediaType == typeInfo.mediaType; - } - - return item.Type == typeInfo.type; - }); - } - - function canPlaySomeItemInCollection(items) { - let i = 0; - - for (let length = items.length; i < length; i++) { - if (playbackManager.canPlay(items[i])) { - return true; - } - } - - return false; - } - - function renderCollectionItems(page, parentItem, types, items) { - page.querySelector('.collectionItems').classList.remove('hide'); - page.querySelector('.collectionItems').innerHTML = ''; - - for (const type of types) { - const typeItems = filterItemsByCollectionItemType(items, type); - - if (typeItems.length) { - renderCollectionItemType(page, parentItem, type, typeItems); - } - } - - const otherType = { - name: globalize.translate('HeaderOtherItems') - }; - const otherTypeItems = items.filter(function (curr) { - return !types.filter(function (t) { - return filterItemsByCollectionItemType([curr], t).length > 0; - }).length; - }); - - if (otherTypeItems.length) { - renderCollectionItemType(page, parentItem, otherType, otherTypeItems); - } - - if (!items.length) { - renderCollectionItemType(page, parentItem, { - name: globalize.translate('Items') - }, items); - } - - const containers = page.querySelectorAll('.collectionItemsContainer'); - - const notifyRefreshNeeded = function () { - renderChildren(page, parentItem); - }; - - for (const container of containers) { - container.notifyRefreshNeeded = notifyRefreshNeeded; - } - - // if nothing in the collection can be played hide play and shuffle buttons - if (!canPlaySomeItemInCollection(items)) { - hideAll(page, 'btnPlay', false); - hideAll(page, 'btnShuffle', false); - } - - // HACK: Call autoFocuser again because btnPlay may be hidden, but focused by reloadFromItem - // FIXME: Sometimes focus does not move until all (?) sections are loaded - import('autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); - } - - function renderCollectionItemType(page, parentItem, type, items) { - let html = ''; - html += '
'; - html += '
'; - html += '

'; - html += '' + type.name + ''; - html += '

'; - html += '
'; - html += '
'; - const shape = type.type == 'MusicAlbum' ? getSquareShape(false) : getPortraitShape(false); - html += cardBuilder.getCardsHtml({ - items: items, - shape: shape, - showTitle: true, - showYear: type.mediaType === 'Video' || type.type === 'Series', - centerText: true, - lazy: true, - showDetailsMenu: true, - overlayMoreButton: true, - showAddToCollection: false, - showRemoveFromCollection: true, - collectionId: parentItem.Id - }); - html += '
'; - html += '
'; - const collectionItems = page.querySelector('.collectionItems'); - collectionItems.insertAdjacentHTML('beforeend', html); - imageLoader.lazyChildren(collectionItems); - } - - function renderMusicVideos(page, item, user) { - connectionManager.getApiClient(item.ServerId).getItems(user.Id, { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'MusicVideo', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount', - AlbumIds: item.Id - }).then(function (result) { - if (result.Items.length) { - page.querySelector('#musicVideosCollapsible').classList.remove('hide'); - const musicVideosContent = page.querySelector('.musicVideosContent'); - musicVideosContent.innerHTML = getVideosHtml(result.Items); - imageLoader.lazyChildren(musicVideosContent); - } else { - page.querySelector('#musicVideosCollapsible').classList.add('hide'); - } - }); - } - - function renderAdditionalParts(page, item, user) { - connectionManager.getApiClient(item.ServerId).getAdditionalVideoParts(user.Id, item.Id).then(function (result) { - if (result.Items.length) { - page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); - const additionalPartsContent = page.querySelector('#additionalPartsContent'); - additionalPartsContent.innerHTML = getVideosHtml(result.Items); - imageLoader.lazyChildren(additionalPartsContent); - } else { - page.querySelector('#additionalPartsCollapsible').classList.add('hide'); - } - }); - } - - function renderScenes(page, item) { - let chapters = item.Chapters || []; - - if (chapters.length && !chapters[0].ImageTag && (chapters = []), chapters.length) { - page.querySelector('#scenesCollapsible').classList.remove('hide'); - const scenesContent = page.querySelector('#scenesContent'); - - import('chaptercardbuilder').then(({default: chaptercardbuilder}) => { - chaptercardbuilder.buildChapterCards(item, chapters, { - itemsContainer: scenesContent, - backdropShape: 'overflowBackdrop', - squareShape: 'overflowSquare', - imageBlurhashes: item.ImageBlurHashes - }); - }); +function renderSeriesSchedule(page, item) { + const apiClient = connectionManager.getApiClient(item.ServerId); + apiClient.getLiveTvPrograms({ + UserId: apiClient.getCurrentUserId(), + HasAired: false, + SortBy: 'StartDate', + EnableTotalRecordCount: false, + EnableImages: false, + ImageTypeLimit: 0, + Limit: 50, + EnableUserData: false, + LibrarySeriesId: item.Id + }).then(function (result) { + if (result.Items.length) { + page.querySelector('#seriesScheduleSection').classList.remove('hide'); } else { - page.querySelector('#scenesCollapsible').classList.add('hide'); + page.querySelector('#seriesScheduleSection').classList.add('hide'); } - } - function getVideosHtml(items) { - return cardBuilder.getCardsHtml({ - items: items, - shape: 'autooverflow', + page.querySelector('#seriesScheduleList').innerHTML = listView.getListViewHtml({ + items: result.Items, + enableUserDataButtons: false, + showParentTitle: false, + image: false, + showProgramDateTime: true, + mediaInfo: false, showTitle: true, - action: 'play', - overlayText: false, - centerText: true, - showRuntime: true + moreButton: false, + action: 'programdialog' }); + loading.hide(); + }); +} + +function inferContext(item) { + if (item.Type === 'Movie' || item.Type === 'BoxSet') { + return 'movies'; } - function renderSpecials(page, item, user) { - connectionManager.getApiClient(item.ServerId).getSpecialFeatures(user.Id, item.Id).then(function (specials) { - const specialsContent = page.querySelector('#specialsContent'); - specialsContent.innerHTML = getVideosHtml(specials); - imageLoader.lazyChildren(specialsContent); - }); + if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') { + return 'tvshows'; } - function renderCast(page, item) { - const people = (item.People || []).filter(function (p) { - return p.Type === 'Actor'; - }); + if (item.Type === 'MusicArtist' || item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'AudioBook') { + return 'music'; + } - if (!people.length) { - return void page.querySelector('#castCollapsible').classList.add('hide'); + if (item.Type === 'Program') { + return 'livetv'; + } + + return null; +} + +function filterItemsByCollectionItemType(items, typeInfo) { + return items.filter(function (item) { + if (typeInfo.mediaType) { + return item.MediaType == typeInfo.mediaType; } - page.querySelector('#castCollapsible').classList.remove('hide'); - const castContent = page.querySelector('#castContent'); + return item.Type == typeInfo.type; + }); +} - import('peoplecardbuilder').then(({default: peoplecardbuilder}) => { - peoplecardbuilder.buildPeopleCards(people, { - itemsContainer: castContent, - coverImage: true, - serverId: item.ServerId, - shape: 'overflowPortrait', +function canPlaySomeItemInCollection(items) { + let i = 0; + + for (let length = items.length; i < length; i++) { + if (playbackManager.canPlay(items[i])) { + return true; + } + } + + return false; +} + +function renderCollectionItems(page, parentItem, types, items) { + page.querySelector('.collectionItems').classList.remove('hide'); + page.querySelector('.collectionItems').innerHTML = ''; + + for (const type of types) { + const typeItems = filterItemsByCollectionItemType(items, type); + + if (typeItems.length) { + renderCollectionItemType(page, parentItem, type, typeItems); + } + } + + const otherType = { + name: globalize.translate('HeaderOtherItems') + }; + const otherTypeItems = items.filter(function (curr) { + return !types.filter(function (t) { + return filterItemsByCollectionItemType([curr], t).length > 0; + }).length; + }); + + if (otherTypeItems.length) { + renderCollectionItemType(page, parentItem, otherType, otherTypeItems); + } + + if (!items.length) { + renderCollectionItemType(page, parentItem, { + name: globalize.translate('Items') + }, items); + } + + const containers = page.querySelectorAll('.collectionItemsContainer'); + + const notifyRefreshNeeded = function () { + renderChildren(page, parentItem); + }; + + for (const container of containers) { + container.notifyRefreshNeeded = notifyRefreshNeeded; + } + + // if nothing in the collection can be played hide play and shuffle buttons + if (!canPlaySomeItemInCollection(items)) { + hideAll(page, 'btnPlay', false); + hideAll(page, 'btnShuffle', false); + } + + // HACK: Call autoFocuser again because btnPlay may be hidden, but focused by reloadFromItem + // FIXME: Sometimes focus does not move until all (?) sections are loaded + import('autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); +} + +function renderCollectionItemType(page, parentItem, type, items) { + let html = ''; + html += '
'; + html += '
'; + html += '

'; + html += '' + type.name + ''; + html += '

'; + html += '
'; + html += '
'; + const shape = type.type == 'MusicAlbum' ? getSquareShape(false) : getPortraitShape(false); + html += cardBuilder.getCardsHtml({ + items: items, + shape: shape, + showTitle: true, + showYear: type.mediaType === 'Video' || type.type === 'Series', + centerText: true, + lazy: true, + showDetailsMenu: true, + overlayMoreButton: true, + showAddToCollection: false, + showRemoveFromCollection: true, + collectionId: parentItem.Id + }); + html += '
'; + html += '
'; + const collectionItems = page.querySelector('.collectionItems'); + collectionItems.insertAdjacentHTML('beforeend', html); + imageLoader.lazyChildren(collectionItems); +} + +function renderMusicVideos(page, item, user) { + connectionManager.getApiClient(item.ServerId).getItems(user.Id, { + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'MusicVideo', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount', + AlbumIds: item.Id + }).then(function (result) { + if (result.Items.length) { + page.querySelector('#musicVideosCollapsible').classList.remove('hide'); + const musicVideosContent = page.querySelector('.musicVideosContent'); + musicVideosContent.innerHTML = getVideosHtml(result.Items); + imageLoader.lazyChildren(musicVideosContent); + } else { + page.querySelector('#musicVideosCollapsible').classList.add('hide'); + } + }); +} + +function renderAdditionalParts(page, item, user) { + connectionManager.getApiClient(item.ServerId).getAdditionalVideoParts(user.Id, item.Id).then(function (result) { + if (result.Items.length) { + page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); + const additionalPartsContent = page.querySelector('#additionalPartsContent'); + additionalPartsContent.innerHTML = getVideosHtml(result.Items); + imageLoader.lazyChildren(additionalPartsContent); + } else { + page.querySelector('#additionalPartsCollapsible').classList.add('hide'); + } + }); +} + +function renderScenes(page, item) { + let chapters = item.Chapters || []; + + if (chapters.length && !chapters[0].ImageTag && (chapters = []), chapters.length) { + page.querySelector('#scenesCollapsible').classList.remove('hide'); + const scenesContent = page.querySelector('#scenesContent'); + + import('chaptercardbuilder').then(({ default: chaptercardbuilder }) => { + chaptercardbuilder.buildChapterCards(item, chapters, { + itemsContainer: scenesContent, + backdropShape: 'overflowBackdrop', + squareShape: 'overflowSquare', imageBlurhashes: item.ImageBlurHashes }); }); + } else { + page.querySelector('#scenesCollapsible').classList.add('hide'); + } +} + +function getVideosHtml(items) { + return cardBuilder.getCardsHtml({ + items: items, + shape: 'autooverflow', + showTitle: true, + action: 'play', + overlayText: false, + centerText: true, + showRuntime: true + }); +} + +function renderSpecials(page, item, user) { + connectionManager.getApiClient(item.ServerId).getSpecialFeatures(user.Id, item.Id).then(function (specials) { + const specialsContent = page.querySelector('#specialsContent'); + specialsContent.innerHTML = getVideosHtml(specials); + imageLoader.lazyChildren(specialsContent); + }); +} + +function renderCast(page, item) { + const people = (item.People || []).filter(function (p) { + return p.Type === 'Actor'; + }); + + if (!people.length) { + return void page.querySelector('#castCollapsible').classList.add('hide'); } - function itemDetailPage() { - const self = this; - self.setInitialCollapsibleState = setInitialCollapsibleState; - self.renderDetails = renderDetails; - self.renderCast = renderCast; + page.querySelector('#castCollapsible').classList.remove('hide'); + const castContent = page.querySelector('#castContent'); + + import('peoplecardbuilder').then(({ default: peoplecardbuilder }) => { + peoplecardbuilder.buildPeopleCards(people, { + itemsContainer: castContent, + coverImage: true, + serverId: item.ServerId, + shape: 'overflowPortrait', + imageBlurhashes: item.ImageBlurHashes + }); + }); +} + +function itemDetailPage() { + const self = this; + self.setInitialCollapsibleState = setInitialCollapsibleState; + self.renderDetails = renderDetails; + self.renderCast = renderCast; +} + +function bindAll(view, selector, eventName, fn) { + const elems = view.querySelectorAll(selector); + + for (const elem of elems) { + elem.addEventListener(eventName, fn); } +} - function bindAll(view, selector, eventName, fn) { - const elems = view.querySelectorAll(selector); +function onTrackSelectionsSubmit(e) { + e.preventDefault(); + return false; +} - for (const elem of elems) { - elem.addEventListener(eventName, fn); - } - } +window.ItemDetailPage = new itemDetailPage(); - function onTrackSelectionsSubmit(e) { - e.preventDefault(); - return false; - } +export default function (view, params) { + function reload(instance, page, params) { + loading.show(); - window.ItemDetailPage = new itemDetailPage(); - - export default function (view, params) { - function reload(instance, page, params) { - loading.show(); - - const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; - - Promise.all([getPromise(apiClient, params), apiClient.getCurrentUser()]).then(([item, user]) => { - currentItem = item; - reloadFromItem(instance, page, params, item, user); - }).catch((error) => { - console.error('failed to get item or current user: ', error); - }); - } - - function splitVersions(instance, page, apiClient, params) { - import('confirm').then(({default: confirm}) => { - confirm('Are you sure you wish to split the media sources into separate items?', 'Split Media Apart').then(function () { - loading.show(); - apiClient.ajax({ - type: 'DELETE', - url: apiClient.getUrl('Videos/' + params.id + '/AlternateSources') - }).then(function () { - loading.hide(); - reload(instance, page, params); - }); - }); - }); - } - - function getPlayOptions(startPosition) { - const audioStreamIndex = view.querySelector('.selectAudio').value || null; - return { - startPositionTicks: startPosition, - mediaSourceId: view.querySelector('.selectSource').value, - audioStreamIndex: audioStreamIndex, - subtitleStreamIndex: view.querySelector('.selectSubtitles').value - }; - } - - function playItem(item, startPosition) { - const playOptions = getPlayOptions(startPosition); - playOptions.items = [item]; - playbackManager.play(playOptions); - } - - function playTrailer() { - playbackManager.playTrailers(currentItem); - } - - function playCurrentItem(button, mode) { - const item = currentItem; - - if (item.Type === 'Program') { - const apiClient = connectionManager.getApiClient(item.ServerId); - return void apiClient.getLiveTvChannel(item.ChannelId, apiClient.getCurrentUserId()).then(function (channel) { - playbackManager.play({ - items: [channel] - }); - }); - } - - playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); - } - - function onPlayClick() { - playCurrentItem(this, this.getAttribute('data-mode')); - } - - function onPosterClick(e) { - itemShortcuts.onClick.call(view.querySelector('.detailImageContainer'), e); - } - - function onInstantMixClick() { - playbackManager.instantMix(currentItem); - } - - function onShuffleClick() { - playbackManager.shuffle(currentItem); - } - - function onCancelSeriesTimerClick() { - import('recordingHelper').then(({default: recordingHelper}) => { - recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () { - Dashboard.navigate('livetv.html'); - }); - }); - } - - function onCancelTimerClick() { - import('recordingHelper').then(({default: recordingHelper}) => { - recordingHelper.cancelTimer(connectionManager.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () { - reload(self, view, params); - }); - }); - } - - function onPlayTrailerClick() { - playTrailer(); - } - - function onDownloadClick() { - import('fileDownloader').then(({default: fileDownloader}) => { - const downloadHref = apiClient.getItemDownloadUrl(currentItem.Id); - fileDownloader.download([{ - url: downloadHref, - itemId: currentItem.Id, - serverId: currentItem.serverId - }]); - }); - } - - function onMoreCommandsClick() { - var button = this; - var selectedItem = currentItem; - apiClient.getItem(apiClient.getCurrentUserId(), view.querySelector('.selectSource').value).then(function (item) { - selectedItem = item; - - apiClient.getCurrentUser().then(function (user) { - itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)).then(function (result) { - if (result.deleted) { - appRouter.goHome(); - } else if (result.updated) { - reload(self, view, params); - } - }); - }); - }); - } - - function onPlayerChange() { - renderTrackSelections(view, self, currentItem); - setTrailerButtonVisibility(view, currentItem); - } - - function onWebSocketMessage(e, data) { - const msg = data; - - if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) { - const key = currentItem.UserData.Key; - const userData = msg.Data.UserDataList.filter(function (u) { - return u.Key == key; - })[0]; - - if (userData) { - currentItem.UserData = userData; - reloadPlayButtons(view, currentItem); - refreshImage(view, currentItem); - } - } - } - - let currentItem; - const self = this; const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; - view.querySelectorAll('.btnPlay'); - bindAll(view, '.btnPlay', 'click', onPlayClick); - bindAll(view, '.btnResume', 'click', onPlayClick); - bindAll(view, '.btnInstantMix', 'click', onInstantMixClick); - bindAll(view, '.btnShuffle', 'click', onShuffleClick); - bindAll(view, '.btnPlayTrailer', 'click', onPlayTrailerClick); - bindAll(view, '.btnCancelSeriesTimer', 'click', onCancelSeriesTimerClick); - bindAll(view, '.btnCancelTimer', 'click', onCancelTimerClick); - bindAll(view, '.btnDownload', 'click', onDownloadClick); - view.querySelector('.detailImageContainer').addEventListener('click', onPosterClick); - view.querySelector('.trackSelections').addEventListener('submit', onTrackSelectionsSubmit); - view.querySelector('.btnSplitVersions').addEventListener('click', function () { - splitVersions(self, view, apiClient, params); - }); - bindAll(view, '.btnMoreCommands', 'click', onMoreCommandsClick); - view.querySelector('.selectSource').addEventListener('change', function () { - renderVideoSelections(view, self._currentPlaybackMediaSources); - renderAudioSelections(view, self._currentPlaybackMediaSources); - renderSubtitleSelections(view, self._currentPlaybackMediaSources); - }); - view.addEventListener('viewshow', function (e) { - const page = this; - libraryMenu.setTransparentMenu(true); - - if (e.detail.isRestored) { - if (currentItem) { - Emby.Page.setTitle(''); - renderTrackSelections(page, self, currentItem, true); - } - } else { - reload(self, page, params); - } - - events.on(apiClient, 'message', onWebSocketMessage); - events.on(playbackManager, 'playerchange', onPlayerChange); - }); - view.addEventListener('viewbeforehide', function () { - events.off(apiClient, 'message', onWebSocketMessage); - events.off(playbackManager, 'playerchange', onPlayerChange); - libraryMenu.setTransparentMenu(false); - }); - view.addEventListener('viewdestroy', function () { - currentItem = null; - self._currentPlaybackMediaSources = null; - self.currentRecordingFields = null; + Promise.all([getPromise(apiClient, params), apiClient.getCurrentUser()]).then(([item, user]) => { + currentItem = item; + reloadFromItem(instance, page, params, item, user); + }).catch((error) => { + console.error('failed to get item or current user: ', error); }); } -/* eslint-enable indent */ + function splitVersions(instance, page, apiClient, params) { + import('confirm').then(({ default: confirm }) => { + confirm('Are you sure you wish to split the media sources into separate items?', 'Split Media Apart').then(function () { + loading.show(); + apiClient.ajax({ + type: 'DELETE', + url: apiClient.getUrl('Videos/' + params.id + '/AlternateSources') + }).then(function () { + loading.hide(); + reload(instance, page, params); + }); + }); + }); + } + + function getPlayOptions(startPosition) { + const audioStreamIndex = view.querySelector('.selectAudio').value || null; + return { + startPositionTicks: startPosition, + mediaSourceId: view.querySelector('.selectSource').value, + audioStreamIndex: audioStreamIndex, + subtitleStreamIndex: view.querySelector('.selectSubtitles').value + }; + } + + function playItem(item, startPosition) { + const playOptions = getPlayOptions(startPosition); + playOptions.items = [item]; + playbackManager.play(playOptions); + } + + function playTrailer() { + playbackManager.playTrailers(currentItem); + } + + function playCurrentItem(button, mode) { + const item = currentItem; + + if (item.Type === 'Program') { + const apiClient = connectionManager.getApiClient(item.ServerId); + return void apiClient.getLiveTvChannel(item.ChannelId, apiClient.getCurrentUserId()).then(function (channel) { + playbackManager.play({ + items: [channel] + }); + }); + } + + playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); + } + + function onPlayClick() { + playCurrentItem(this, this.getAttribute('data-mode')); + } + + function onPosterClick(e) { + itemShortcuts.onClick.call(view.querySelector('.detailImageContainer'), e); + } + + function onInstantMixClick() { + playbackManager.instantMix(currentItem); + } + + function onShuffleClick() { + playbackManager.shuffle(currentItem); + } + + function onCancelSeriesTimerClick() { + import('recordingHelper').then(({ default: recordingHelper }) => { + recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () { + Dashboard.navigate('livetv.html'); + }); + }); + } + + function onCancelTimerClick() { + import('recordingHelper').then(({ default: recordingHelper }) => { + recordingHelper.cancelTimer(connectionManager.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () { + reload(self, view, params); + }); + }); + } + + function onPlayTrailerClick() { + playTrailer(); + } + + function onDownloadClick() { + import('fileDownloader').then(({ default: fileDownloader }) => { + const downloadHref = apiClient.getItemDownloadUrl(currentItem.Id); + fileDownloader.download([{ + url: downloadHref, + itemId: currentItem.Id, + serverId: currentItem.serverId + }]); + }); + } + + function onMoreCommandsClick() { + const button = this; + let selectedItem = view.querySelector('.selectSource').value || currentItem.Id; + + apiClient.getItem(apiClient.getCurrentUserId(), selectedItem).then(function (item) { + selectedItem = item; + + apiClient.getCurrentUser().then(function (user) { + itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)).then(function (result) { + if (result.deleted) { + appRouter.goHome(); + } else if (result.updated) { + reload(self, view, params); + } + }); + }); + }); + } + + function onPlayerChange() { + renderTrackSelections(view, self, currentItem); + setTrailerButtonVisibility(view, currentItem); + } + + function onWebSocketMessage(e, data) { + const msg = data; + + if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) { + const key = currentItem.UserData.Key; + const userData = msg.Data.UserDataList.filter(function (u) { + return u.Key == key; + })[0]; + + if (userData) { + currentItem.UserData = userData; + reloadPlayButtons(view, currentItem); + refreshImage(view, currentItem); + } + } + } + + let currentItem; + const self = this; + const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; + view.querySelectorAll('.btnPlay'); + bindAll(view, '.btnPlay', 'click', onPlayClick); + bindAll(view, '.btnResume', 'click', onPlayClick); + bindAll(view, '.btnInstantMix', 'click', onInstantMixClick); + bindAll(view, '.btnShuffle', 'click', onShuffleClick); + bindAll(view, '.btnPlayTrailer', 'click', onPlayTrailerClick); + bindAll(view, '.btnCancelSeriesTimer', 'click', onCancelSeriesTimerClick); + bindAll(view, '.btnCancelTimer', 'click', onCancelTimerClick); + bindAll(view, '.btnDownload', 'click', onDownloadClick); + view.querySelector('.detailImageContainer').addEventListener('click', onPosterClick); + view.querySelector('.trackSelections').addEventListener('submit', onTrackSelectionsSubmit); + view.querySelector('.btnSplitVersions').addEventListener('click', function () { + splitVersions(self, view, apiClient, params); + }); + bindAll(view, '.btnMoreCommands', 'click', onMoreCommandsClick); + view.querySelector('.selectSource').addEventListener('change', function () { + renderVideoSelections(view, self._currentPlaybackMediaSources); + renderAudioSelections(view, self._currentPlaybackMediaSources); + renderSubtitleSelections(view, self._currentPlaybackMediaSources); + }); + view.addEventListener('viewshow', function (e) { + const page = this; + + libraryMenu.setTransparentMenu(true); + + if (e.detail.isRestored) { + if (currentItem) { + Emby.Page.setTitle(''); + renderTrackSelections(page, self, currentItem, true); + } + } else { + reload(self, page, params); + } + + events.on(apiClient, 'message', onWebSocketMessage); + events.on(playbackManager, 'playerchange', onPlayerChange); + }); + view.addEventListener('viewbeforehide', function () { + events.off(apiClient, 'message', onWebSocketMessage); + events.off(playbackManager, 'playerchange', onPlayerChange); + libraryMenu.setTransparentMenu(false); + }); + view.addEventListener('viewdestroy', function () { + currentItem = null; + self._currentPlaybackMediaSources = null; + self.currentRecordingFields = null; + }); +} From d25850b6969c3c103fb3c595304fdc11eea176aa Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:14:53 -0400 Subject: [PATCH 08/31] Remove un-needed styles. --- src/assets/css/videoosd.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 2a28e9cff8..808915e58b 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -248,8 +248,6 @@ } @media all and (max-width: 30em) { - .osdControls .btnFastForward, - .osdControls .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; From dbf1d4f811a762681497640c4b49fdca53a99dbd Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:21:31 -0400 Subject: [PATCH 09/31] Add mediaType to isPlaying for remote players. --- src/plugins/chromecastPlayer/plugin.js | 4 ++-- src/plugins/sessionPlayer/plugin.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b7e6d05969..5ba8596089 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -931,9 +931,9 @@ class ChromecastPlayer { return state.VolumeLevel == null ? 100 : state.VolumeLevel; } - isPlaying() { + isPlaying(mediaType) { const state = this.lastPlayerData || {}; - return state.NowPlayingItem != null; + return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType); } isPlayingVideo() { diff --git a/src/plugins/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index cbeb6f34b4..7e5dc9f4f7 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -466,9 +466,9 @@ class SessionPlayer { sendCommandByName(this, 'DisplayContent', options); } - isPlaying() { + isPlaying(mediaType) { const state = this.lastPlayerData || {}; - return state.NowPlayingItem != null; + return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType); } isPlayingVideo() { From 7eff82d671d2df4c65988e229984af98a15189b8 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:27:28 -0400 Subject: [PATCH 10/31] Add null check to remotecontrol. --- src/components/remotecontrol/remotecontrol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 8e1eb55a1d..fcadadcf30 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -134,7 +134,7 @@ function imageUrl(item, options) { function updateNowPlayingInfo(context, state, serverId) { const item = state.NowPlayingItem; const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; - if (typeof item !== 'undefined') { + if (typeof item !== 'undefined' && item !== null) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { const songName = item.Name; From 1a360a6f72b0383320756d672d2dee8e10529e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B9=20=D0=98=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Wed, 26 Aug 2020 13:31:59 +0000 Subject: [PATCH 11/31] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/ --- src/strings/bg-bg.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/strings/bg-bg.json b/src/strings/bg-bg.json index a79650546c..4e7e4407be 100644 --- a/src/strings/bg-bg.json +++ b/src/strings/bg-bg.json @@ -188,7 +188,7 @@ "HeaderPreferredMetadataLanguage": "Предпочитан език на метаданните", "HeaderProfile": "Профил", "HeaderProfileInformation": "Профил", - "HeaderProfileServerSettingsHelp": "Тези величини определят как Jellyfin сървърът ще се представя на устройствата.", + "HeaderProfileServerSettingsHelp": "Тези величини определят как Джелифин сървърът ще се представя на устройствата.", "HeaderRecentlyPlayed": "Скоро пускани", "HeaderRemoteControl": "Отдалечен контрол", "HeaderRemoveMediaFolder": "Премахване на медийна папка", @@ -228,7 +228,7 @@ "Horizontal": "Водоравно", "Identify": "Разпознаване", "Images": "Изображения", - "ImportMissingEpisodesHelp": "Ако е активирано, информация за липсващи епизоди ще бъде добавена в базата данни на Jellyfin и ще бъде показвана заедно със сезони и серии. Това може да доведе до значително по-дълго сканиране на библиотеката.", + "ImportMissingEpisodesHelp": "Ако е активирано, информация за липсващи епизоди ще бъде добавена в базата данни на Джелифин и ще бъде показвана заедно със сезони и серии. Това може да доведе до значително по-дълго сканиране на библиотеката.", "InstallingPackage": "Инсталиране на {0} на версия {1})", "InstantMix": "Пускане на подобни", "Label3DFormat": "Триизмерен формат:", @@ -262,7 +262,7 @@ "LabelDay": "Ден:", "LabelDeviceDescription": "Описание на устройството", "LabelDisplayLanguage": "Език на показване:", - "LabelDisplayLanguageHelp": "Превеждането на Емби е текущ проект.", + "LabelDisplayLanguageHelp": "Превеждането на Джелифин е текущ проект.", "LabelDisplayMode": "Режим на показване:", "LabelDisplayName": "Показвано име:", "LabelDisplayOrder": "Ред на показване:", @@ -275,7 +275,7 @@ "LabelEmbedAlbumArtDidl": "Вградждане на албумно изкуство в Didl", "LabelEnableAutomaticPortMap": "Автоматично съответстване на портовете", "LabelEnableDlnaClientDiscoveryInterval": "Интервал за откриване на клиенти (секунди)", - "LabelEnableDlnaClientDiscoveryIntervalHelp": "Определя времетраенето в секунди между SSDP търсения направени от Jellyfin.", + "LabelEnableDlnaClientDiscoveryIntervalHelp": "Определя времетраенето в секунди между SSDP търсения направени от Джелифин.", "LabelEnableDlnaDebugLogging": "Включване на журналите за грешки на ДЛНА", "LabelEnableDlnaPlayTo": "Включване на функцията \"възпроизвеждане с ДЛНА\"", "LabelEnableDlnaPlayToHelp": "Засичане на устройства в мрежата ви и предлагане на възможност за дистанционно управление.", @@ -585,8 +585,8 @@ "SearchForSubtitles": "Търсене на субтитри", "SendMessage": "Изпращане на съобщение", "SeriesYearToPresent": "{0} - Настояще", - "ServerNameIsRestarting": "Сървърно издание Емби - {0} се пуска повторно.", - "ServerNameIsShuttingDown": "Сървърно издание Емби - {0} се изключва.", + "ServerNameIsRestarting": "Сървърът - {0} се пуска повторно.", + "ServerNameIsShuttingDown": "Сървърът във - {0} се изключва.", "ServerRestartNeededAfterPluginInstall": "След инсталирането на приставка, сървърът ще трябва да бъде пуснат наново.", "ServerUpdateNeeded": "Сървърът трябва да бъде обновен. Моля, посетете {0}, за да свалите последната версия.", "Settings": "Настройки", @@ -676,7 +676,7 @@ "ViewAlbum": "Преглед на албума", "Watched": "Изгледано", "Wednesday": "Сряда", - "WelcomeToProject": "Добре дошли в Емби!", + "WelcomeToProject": "Добре дошли в Джелифин!", "WizardCompleted": "Това е всичко, от което се нуждаем за момента. Джелифин започва да събира данни за библиотеката ви. Разгледайте някои от нашите приложения, после натиснете Готово, за да видите таблото на сървъра.", "Writer": "Писател", "AllowMediaConversion": "Разрешаване на медийни преобразувания", @@ -736,7 +736,7 @@ "ButtonAddImage": "Добавяне на изображение", "MessageBrowsePluginCatalog": "За да видите наличните добавки, прегледайте каталога с добавките.", "Box": "Кутия", - "AlwaysPlaySubtitlesHelp": "Поднадписите, съвпадащи с езика от настройките, ще се зареждат, независимо от езика на аудио то.", + "AlwaysPlaySubtitlesHelp": "Субтитрите, съвпадащи с езика от настройките, ще се зареждат, независимо от езика на аудиото.", "BookLibraryHelp": "Поддържат се аудио книги такива съдържащи текст. Проверете ръководството за наименуване {1} на книги {0}.", "Blacklist": "Списък с блокирани", "BirthLocation": "Месторождение", @@ -751,7 +751,7 @@ "AnyLanguage": "Който и да е език", "AlwaysPlaySubtitles": "Постоянно изпълнение", "AllowRemoteAccessHelp": "Ако не е маркирано, всеки отдалечен достъп ще бъде блокиран.", - "AllowRemoteAccess": "Позволяване на отдалечен достъп до този Jellyfin сървър.", + "AllowRemoteAccess": "Позволяване на отдалечен достъп до този Джелифин сървър.", "AllowFfmpegThrottling": "Подтискане на прекодирането", "AllowMediaConversionHelp": "Даване или отнемане на права за функциите за конвертиране на медия.", "AlbumArtist": "Изпълнител", @@ -767,7 +767,7 @@ "ButtonEditOtherUserPreferences": "Редакция на потребителския профил, изображение и лични предпочитания.", "BoxRear": "Комплект (стар)", "BoxSet": "Комплект", - "AuthProviderHelp": "Избор на доставчик на услуга за Автентификация, която ще се използва за автентификация на потребителската парола.", + "AuthProviderHelp": "Избор на доставчик на услуга за автентификация, която ще се използва за автентификация на потребителската парола.", "AllowedRemoteAddressesHelp": "Списък с IP адреси или IP/маска записи, разделени със запетая, които ще имат отдалечен достъп. Ако полето не е попълнено всички адреси ще имат отдалечен достъп.", "BurnSubtitlesHelp": "Определя дали сървърът трябва да записва субтитри във видеофайлове при прекодиране. Избягването на това значително ще подобри производителността. Изберете Auto, за да запишете формати, базирани на изображения (VOBSUB, PGS, SUB, IDX) и някои ASS или SSA субтитри.", "AllowFfmpegThrottlingHelp": "Когато прекодирането или запазването на видео стигне достатъчно далеч напред от текущата позиция за възпроизвеждане, поставете на пауза процеса, така ще се изразходват по-малко ресурси. Това е най-полезно, когато се гледа, без да се търси често из видеото. Изключете това, ако имате проблеми с възпроизвеждането.", @@ -780,10 +780,10 @@ "CopyStreamURL": "Копиране URL на стрийма", "CopyStreamURLSuccess": "URL се копира успешно.", "Connect": "Свързване", - "ConfirmEndPlayerSession": "Искате ли да изключите Jellyfin на {0}?", + "ConfirmEndPlayerSession": "Искате ли да изключите Джелифин на {0}?", "ConfirmDeletion": "Потвърждаване на изтриването", "ConfirmDeleteItem": "Изтриването на елемента ще го премахне едновременно от файловата система и библиотеката. Сигурни ли сте, че искате да продължите?", - "ConfigureDateAdded": "Конфигурацията на добавянето на датата се определя в панела на Jellyfin сървъра в секцията за настройка на библиотека", + "ConfigureDateAdded": "Конфигурацията на добавянето на датата се определя в панела на Джелифин сървъра в секцията за настройка на библиотека", "ConfirmDeleteItems": "Изтриването на елементите ще ги премахне едновременно от файловата система и библиотеката. Сигурни ли сте, че искате да продължите?", "ColorTransfer": "Предаване на цвета", "ColorPrimaries": "Основни цветове", From 9b6b79628f89bb6b7d87acf31187fef13f356c7a Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 17:05:01 -0400 Subject: [PATCH 12/31] Switch to simple check for item. --- src/components/remotecontrol/remotecontrol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index fcadadcf30..befedfe1ac 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -134,7 +134,7 @@ function imageUrl(item, options) { function updateNowPlayingInfo(context, state, serverId) { const item = state.NowPlayingItem; const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; - if (typeof item !== 'undefined' && item !== null) { + if (item) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { const songName = item.Name; From c22e3b2cc6b9a7b9a76f4c33fc9a5523936c9c67 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 17:18:23 -0400 Subject: [PATCH 13/31] Fix seriesImageUrl linter issue. --- src/components/remotecontrol/remotecontrol.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index befedfe1ac..b6bc74e98c 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -192,11 +192,11 @@ function updateNowPlayingInfo(context, state, serverId) { context.querySelector('.nowPlayingPageTitle').classList.add('hide'); } - const url = item ? seriesImageUrl(item, { + const url = seriesImageUrl(item, { maxHeight: 300 }) || imageUrl(item, { maxHeight: 300 - }) : null; + }); let contextButton = context.querySelector('.btnToggleContextMenu'); // We remove the previous event listener by replacing the item in each update event From e7b52072940f6236249ccabe6320114398da1fdd Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 18:35:20 -0400 Subject: [PATCH 14/31] Fix clearing backdrop when no item is playing. --- src/components/remotecontrol/remotecontrol.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index b6bc74e98c..8d6c53cead 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -228,18 +228,16 @@ function updateNowPlayingInfo(context, state, serverId) { }); }); setImageUrl(context, state, url); - if (item) { - backdrop.setBackdrops([item]); - apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { - const userData = fullItem.UserData || {}; - const likes = userData.Likes == null ? '' : userData.Likes; - context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - }); - } else { - backdrop.clearBackdrop(); - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - } + backdrop.setBackdrops([item]); + apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { + const userData = fullItem.UserData || {}; + const likes = userData.Likes == null ? '' : userData.Likes; + context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; + }); + } else { + backdrop.clearBackdrop(); + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; } } From c10d4f75a9881fd98767f516298d020ca8eb751a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B9=20=D0=98=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Wed, 26 Aug 2020 18:57:38 +0000 Subject: [PATCH 15/31] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/ --- src/strings/bg-bg.json | 66 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/strings/bg-bg.json b/src/strings/bg-bg.json index 4e7e4407be..a1909065a0 100644 --- a/src/strings/bg-bg.json +++ b/src/strings/bg-bg.json @@ -240,7 +240,7 @@ "LabelAlbumArtists": "Изпълнители на албума:", "LabelAppName": "Име", "LabelArtists": "Изпълнители:", - "LabelArtistsHelp": "Отделете няколко с ;", + "LabelArtistsHelp": "Отделете различните изпълнители с \";\".", "LabelAudioLanguagePreference": "Предпочитан език на звука:", "LabelCachePath": "Път към кеша:", "LabelCachePathHelp": "Определете място за сървърните кеш файлове, като изображения. Оставете празно, за да използвате мястото по подразбиране.", @@ -274,7 +274,7 @@ "LabelDropShadow": "Сянка:", "LabelEmbedAlbumArtDidl": "Вградждане на албумно изкуство в Didl", "LabelEnableAutomaticPortMap": "Автоматично съответстване на портовете", - "LabelEnableDlnaClientDiscoveryInterval": "Интервал за откриване на клиенти (секунди)", + "LabelEnableDlnaClientDiscoveryInterval": "Интервал за откриване на клиенти", "LabelEnableDlnaClientDiscoveryIntervalHelp": "Определя времетраенето в секунди между SSDP търсения направени от Джелифин.", "LabelEnableDlnaDebugLogging": "Включване на журналите за грешки на ДЛНА", "LabelEnableDlnaPlayTo": "Включване на функцията \"възпроизвеждане с ДЛНА\"", @@ -282,7 +282,7 @@ "LabelEnableDlnaServer": "Включване на ДЛНА-сървър", "LabelEnableDlnaServerHelp": "Разрешава на UPnP устройства в мрежата да разглеждат и пускат Джелифин съдържание.", "LabelEnableRealtimeMonitor": "Активиране на наблюдение в реално време", - "LabelEnableRealtimeMonitorHelp": "Промените ще бъдат обработени веднага, на поддържани файлови системи.", + "LabelEnableRealtimeMonitorHelp": "Промените във файловете ще бъдат обработени веднага, на поддържаните файлови системи.", "LabelEpisodeNumber": "Номер на епизода:", "LabelEvent": "Събитие:", "LabelEveryXMinutes": "На всеки:", @@ -295,14 +295,14 @@ "LabelHomeNetworkQuality": "Качество на домашната мрежа:", "LabelHomeScreenSectionValue": "Раздел {0} на началния екран:", "LabelHttpsPort": "Локален HTTPS порт:", - "LabelHttpsPortHelp": "TCP портът на който HTTPS сървърът на Джелифин трябва да се закачи.", + "LabelHttpsPortHelp": "TCP порта на HTTPS сървъра.", "LabelImageType": "Вид изображение:", "LabelInternetQuality": "Качество на интернетната връзка:", "LabelKodiMetadataDateFormat": "Формат на датата на издаване:", "LabelKodiMetadataUserHelp": "Разрешете това, за да запазите данните за гледанията във файлове Nfo за употреба от други програми.", "LabelLanguage": "Език:", "LabelLocalHttpServerPortNumber": "Локален HTTP порт:", - "LabelLocalHttpServerPortNumberHelp": "TCP портът на който HTTP сървърът на Джелифин трябва да се закачи.", + "LabelLocalHttpServerPortNumberHelp": "TCP порта на HTTP сървъра.", "LabelLogs": "Журнали:", "LabelManufacturer": "Производител:", "LabelManufacturerUrl": "Адрес на производителя", @@ -324,14 +324,14 @@ "LabelModelName": "Модел", "LabelModelNumber": "Номер на модела", "LabelModelUrl": "Адрес на модела", - "LabelMovieRecordingPath": "Път за запис на филмите (по избор):", + "LabelMovieRecordingPath": "Път за запис на филмите:", "LabelName": "Име:", "LabelNewName": "Ново име:", "LabelNewPassword": "Нова парола:", "LabelNewPasswordConfirm": "Нова парола (отново):", "LabelNumberOfGuideDays": "Брой дни за които да се свали програма:", "LabelNumberOfGuideDaysHelp": "Изтеглянето на програма заповече дни дава възможност да планирате по-нататъшните записи предварително, но и отнема повече време, за да се изтегли. Автомат ще избере въз основа на броя на каналите.", - "LabelOptionalNetworkPath": "Споделена мрежова папка (незадължително):", + "LabelOptionalNetworkPath": "Споделена мрежова папка:", "LabelOriginalAspectRatio": "Оригинално съотношение:", "LabelOriginalTitle": "Оригинално заглавие:", "LabelOverview": "Обобщение:", @@ -365,7 +365,7 @@ "LabelSelectUsers": "Избери потребители:", "LabelSelectVersionToInstall": "Изберете версия за инсталиране:", "LabelSerialNumber": "Сериен номер", - "LabelSeriesRecordingPath": "Път за запис на сериалите (по избор):", + "LabelSeriesRecordingPath": "Път за запис на сериалите:", "LabelSkipIfAudioTrackPresent": "Да се пропусне, ако звуковата пътечка по подразбиране съвпада с езика", "LabelSkipIfGraphicalSubsPresent": "Да се пропусне, ако файлът съдържа вградени субтитри", "LabelSortBy": "Подреждане по:", @@ -421,7 +421,7 @@ "Menu": "Меню", "MessageAlreadyInstalled": "Версията вече е инсталирана.", "MessageAreYouSureYouWishToRemoveMediaFolder": "Сигурни ли сте, че искате да премахнете медийната папка?", - "MessageConfirmRestart": "Наистина ли искате да пуснете сървъра наново?", + "MessageConfirmRestart": "Наистина ли искате да рестартирате Джелифин?", "MessageConfirmShutdown": "Наистина ли искате да загасите сървъра?", "MessageNoAvailablePlugins": "Няма налични приставки.", "MessageNoPluginsInstalled": "Нямате инсталирани приставки.", @@ -480,7 +480,7 @@ "OptionDatePlayed": "Дата на пускане", "OptionDescending": "Низходящо", "OptionDisableUser": "Дезактивирайте този потребител", - "OptionDisableUserHelp": "Ако е дезактивиран, сървърът няма да позволи каквито и да било връзки от този потребител. Съществуващите връзки ще бъдат внезапно прекратени.", + "OptionDisableUserHelp": "Сървърът няма да позволи каквито и да било връзки от този потребител. Съществуващите връзки ще бъдат внезапно прекратени.", "OptionDislikes": "Нехаресвания", "OptionDisplayFolderView": "Показване на изглед в папки", "OptionDownloadArtImage": "Картина", @@ -550,7 +550,7 @@ "PlayNextEpisodeAutomatically": "Автоматично пускане на следващия епизод", "Played": "Пускано", "Playlists": "Списъци", - "PleaseRestartServerName": "Моля, пуснете сървъра отново - {0}.", + "PleaseRestartServerName": "Моля, пуснете Джелифин отново - {0}.", "PreferEmbeddedTitlesOverFileNames": "Да се предпочитат вградените заглавия пред имената на файлове", "Premiere": "Премиера", "Premieres": "Премиери", @@ -587,7 +587,7 @@ "SeriesYearToPresent": "{0} - Настояще", "ServerNameIsRestarting": "Сървърът - {0} се пуска повторно.", "ServerNameIsShuttingDown": "Сървърът във - {0} се изключва.", - "ServerRestartNeededAfterPluginInstall": "След инсталирането на приставка, сървърът ще трябва да бъде пуснат наново.", + "ServerRestartNeededAfterPluginInstall": "След инсталирането на приставка, Джелифин ще трябва да бъде пуснат наново.", "ServerUpdateNeeded": "Сървърът трябва да бъде обновен. Моля, посетете {0}, за да свалите последната версия.", "Settings": "Настройки", "SettingsSaved": "Настройките са запазени.", @@ -951,11 +951,11 @@ "LabelBurnSubtitles": "Вграждане на субтитри:", "LabelBlockContentWithTags": "Блокирай елементи с етикети:", "LabelBlastMessageIntervalHelp": "Определя продължителността в секунди при \"бомбардирането\" с активни съобщения.", - "LabelBlastMessageInterval": "Интервал на активните съобщения (в секунди)", + "LabelBlastMessageInterval": "Интервал на активните съобщения", "LabelBitrate": "Битрейт:", "LabelBirthYear": "Година на раждане:", "LabelBirthDate": "Дата на раждане:", - "LabelBindToLocalNetworkAddressHelp": "Не задължително.Замени локалния IP адрес за \"закачане\" към http сървъра.Ако полето е празно сървъра ще \"закачи\" всички налични адреси.Промяната на тази стойност изисква рестарт на сървъра.", + "LabelBindToLocalNetworkAddressHelp": "Замени локалния IP адрес за \"закачане\" към http сървъра.Ако полето е празно сървъра ще \"закачи\" всички налични адреси.Промяната на тази стойност изисква рестарт на сървъра.", "LabelBindToLocalNetworkAddress": "\"Закачи\" към локален мрежов адрес:", "LabelAutomaticallyRefreshInternetMetadataEvery": "Автоматично обновявай метаданните от Интернет:", "LabelAuthProvider": "Доставчик за идентификация:", @@ -994,7 +994,7 @@ "LabelEndDate": "Крайна дата:", "LabelEnableSingleImageInDidlLimitHelp": "Някои устройства няма да го покажат правилно ,ако множество изображения са вградени в Didl.", "LabelEnableSingleImageInDidlLimit": "Ограничи до едно вградено изображение", - "LabelEnableHttpsHelp": "Позволява на сървъра да \"слуша\" на предварително зададен HTTPS порт.Необходимо е да има настроен валиден сертификат ,за да работи правилно настройката.", + "LabelEnableHttpsHelp": "Позволява на сървъра да \"слуша\" на предварително зададен HTTPS порт.Необходимо е да има валиден сертификат ,за да работи правилно настройката.", "LabelEnableHttps": "Включи HTTPS", "LabelEnableHardwareDecodingFor": "Включи хардуерно декодиране за:", "LabelEnableDlnaDebugLoggingHelp": "Създава големи журнали файлове и е редно да се използва само с цел отстраняване на проблеми.", @@ -1080,7 +1080,7 @@ "LabelProtocolInfo": "Информация за протокола:", "LabelPostProcessorArgumentsHelp": "Използвай {path},като път за записване на файла.", "LabelPostProcessorArguments": "Аргументи на командния ред след обработка:", - "EnableBlurHashHelp": "Изображенията, които все още се зареждат, ще се показват чрез функцията\"размито запълване\"", + "EnableBlurHashHelp": "Изображенията, които все още се зареждат, ще се показват чрез функцията\"размито запълване\".", "EnableBlurHash": "Активиране на функцията \"размито запълване\" за изображения", "UnsupportedPlayback": "Джелифин не може да дешифрира съдържание, защитено с DRM, но въпреки това цялото съдържание ще бъде обработено, включително защитените заглавия. Някои файлове могат да изглеждат напълно черни поради криптиране или други неподдържани функции, например интерактивни заглавия.", "OnApplicationStartup": "При стартиране на приложението", @@ -1093,7 +1093,7 @@ "LastSeen": "Последно видян {0}", "PersonRole": "като {0}", "ListPaging": "{0}-{1} от {2}", - "WriteAccessRequired": "Джелифин сървъра изисква достъп с права за запис до тази папка. Моля, осигурете достъп с права за запис и опитайте отново.", + "WriteAccessRequired": "Джелифин изисква достъп с права за запис до тази папка. Моля, осигурете достъп с права за запис и опитайте отново.", "PathNotFound": "Пътят не можа да бъде намерен. Моля, уверете се, че пътят е валиден и опитайте отново.", "Yesterday": "Вчера", "YadifBob": "YADIF х2", @@ -1134,7 +1134,7 @@ "LabelIdentificationFieldHelp": "Подниз или регулярен израз с различаване на главни букви.", "LabelScreensaver": "Скрийнсейвър:", "LabelScheduledTaskLastRan": "Последно пускан {0}, заел {1}.", - "LabelRuntimeMinutes": "Продължителност (в минути):", + "LabelRuntimeMinutes": "Продължителност:", "LabelRequireHttpsHelp": "Ако е отметнато сървъра ще пренасочва автоматично всички заявка от HTTP към HTTPS.Няма никакъв ефект ,ако сървъра не \"слуша\" по HTTPS.", "LabelRequireHttps": "Изисква HTTPS", "LabelRemoteClientBitrateLimitHelp": "Допълнително ограничаване битрейта на поточното предаване за всички мрежови устройства.Това е необходимо ,за да не допуснете устройствата да изискват по-висок битрейт, отколкото вашата интернет връзка позволява.Това може да доведе до повишено натоварване на процесора на вашия сървър, за щото видеата ще се прекодират \"в движение\" до по-нисък битрейт.", @@ -1238,7 +1238,7 @@ "MessageSyncPlayGroupDoesNotExist": "Неуспешно присъединяване към групата, защото не съществува.", "MessageSyncPlayPlaybackPermissionRequired": "Необходимо е разрешение за възпроизвеждане.", "MessageSyncPlayNoGroupsAvailable": "Няма налични групи.Пуснете нещо да се възпроизвежда.", - "MessageSyncPlayGroupWait": "{0} буферира се...", + "MessageSyncPlayGroupWait": "{0} буферира се…", "MessageSyncPlayUserLeft": "{0} е напуснал групата.", "MessageSyncPlayUserJoined": "{0} се присъедини към групата.", "MessageSyncPlayDisabled": "Услугата \"синхронизирано възпроизвеждане\" е изключена.", @@ -1320,7 +1320,7 @@ "Person": "Личност", "PerfectMatch": "Перфектно съвпадение", "PasswordSaved": "Паролата е запазена.", - "PasswordResetProviderHelp": "Изберете доставчик за нулиране на пароли, който да се използва, когато този потребител поиска нулиране на паролата", + "PasswordResetProviderHelp": "Изберете доставчик за нулиране на пароли, който да се използва, когато този потребител поиска нулиране на паролата.", "PasswordResetConfirmation": "Сигурни ли сте, че искате да нулирате паролата?", "PasswordResetComplete": "Паролата е нулирана.", "PasswordMatchError": "Паролата и потвърждението на паролата трябва да съвпадат.", @@ -1344,13 +1344,13 @@ "OptionProtocolHls": "Директно предаване по HTTP", "OptionPosterCard": "Плакат карта", "OptionPoster": "Плакат", - "OptionPlainVideoItemsHelp": "Ако е активирано, всички видеофайлове са представени в DIDL като \"object.item.videoItem\" вместо по-конкретен тип, като например \"object.item.videoItem.movie\".", - "OptionPlainStorageFoldersHelp": "Ако е активирано, всички папки са представени в DIDL като \"object.container.storageFolder\" вместо по-конкретен тип, като например \"object.container.person.musicArtist\".", + "OptionPlainVideoItemsHelp": "Всички видеофайлове са представени в DIDL като \"object.item.videoItem\" вместо по-конкретен тип, като например \"object.item.videoItem.movie\".", + "OptionPlainStorageFoldersHelp": "Всички папки са представени в DIDL като \"object.container.storageFolder\" вместо по-конкретен тип, като например \"object.container.person.musicArtist\".", "OptionMax": "Максимално", "OptionLoginAttemptsBeforeLockoutHelp": "Стойност нула означава наследяване по подразбиране на три опита за нормални потребители и пет за администратори. Задаването на това на -1 ще деактивира функцията.", "OptionLoginAttemptsBeforeLockout": "Определя колко неправилни опита за влизане могат да бъдат направени, преди да бъде блокиран.", "OptionList": "Списък", - "OptionIgnoreTranscodeByteRangeRequestsHelp": "Ако са активирани, тези заявки ще бъдат удовлетворени, но ще се пренебрегне заглавната част от обхвата на байтовете.", + "OptionIgnoreTranscodeByteRangeRequestsHelp": "Тези заявки ще бъдат удовлетворени, но ще се пренебрегне заглавната част от обхвата на байтовете.", "OptionIgnoreTranscodeByteRangeRequests": "Игнорирайте заявките за обхват на байтове при прекодиране", "OptionHlsSegmentedSubtitles": "HLS сегментирани субтитри", "OptionExtractChapterImage": "Включи извличането на изображения от разделите", @@ -1373,12 +1373,12 @@ "OptionBlockLiveTvChannels": "Телевизионни канали на живо", "OptionBlockChannelContent": "Съдържание на интернет канала", "OptionBanner": "Банер", - "OptionAutomaticallyGroupSeriesHelp": "Ако е активирано, сезоните, които се намират в различни папки, ще бъдат автоматично обединени в един сериал.", + "OptionAutomaticallyGroupSeriesHelp": "Сеезоните, които се намират в различни папки, ще бъдат автоматично обединени в един сериал.", "OptionAutomaticallyGroupSeries": "Автоматично обединява сезони, които са разпределени в множество папки", "OptionAllowVideoPlaybackTranscoding": "Разреши възпроизвеждане на видео, което изисква транскодиране", "OptionAllowVideoPlaybackRemuxing": "Разреши възпроизвеждане на видео, което изисква преобразуване без повторно кодиране", "OptionAllowSyncTranscoding": "Разреши изтегляне и синхронизиране на медия, която изисква транскодиране", - "OptionAllowMediaPlaybackTranscodingHelp": "Ограничаването на достъпа до транскодирането може да доведе до проблеми при възпроизвеждането в приложенията на Джелифин поради неподдържани медийни формати.", + "OptionAllowMediaPlaybackTranscodingHelp": "Ограничаването на достъпа до транскодирането може да доведе до проблеми при възпроизвеждането в приложенията поради неподдържани медийни формати.", "OptionAllowLinkSharingHelp": "Споделят се само уеб страници, съдържащи медийна информация.Медийните файлове никога не се споделят публично. Споделянията са ограничени във времето и изтичат след {0} дни.", "OptionAllowContentDownloading": "Разрешаване на изтегляне и синхронизиране на медия", "OptionForceRemoteSourceTranscoding": "Принудително транскодиране на отдалечени медийни източници (като поточна ТВ)", @@ -1428,5 +1428,19 @@ "EnableDecodingColorDepth10Hevc": "Включи 10 битово хардуерно декодиране за HEVC", "ButtonCast": "Стриймване", "ButtonSyncPlay": "SyncPlay", - "TabRepositories": "Хранилища" + "TabRepositories": "Хранилища", + "Preview": "Преглед", + "SubtitleVerticalPositionHelp": "Номер на ред, където се появява текст. Положителните числа показват отгоре надолу. Отрицателните числа показват отдолу нагоре.", + "LabelSubtitleVerticalPosition": "Вертикална позиция:", + "ClearQueue": "Изчисти опашката", + "StopPlayback": "Спри възпроизвеждане", + "ButtonPlayer": "Плеър", + "Writers": "Сценаристи", + "ViewAlbumArtist": "Виж албума на изпълнителя", + "PreviousTrack": "Прескочи към предишен", + "MessageGetInstalledPluginsError": "Възникна грешка при извличане на списъка с инсталираните добавки.", + "MessagePluginInstallError": "Възникна грешка при инсталирането на добавката.", + "PlaybackRate": "Скорост на възпроизвеждане", + "NextTrack": "Прескочи към следващ", + "LabelUnstable": "Нестабилна" } From edbf3f044ef00b1d15bd0097f65864bcf677651f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Wed, 26 Aug 2020 22:36:02 +0000 Subject: [PATCH 16/31] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/ --- src/strings/pt-br.json | 80 ++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index 8019cf9eeb..c5e25e7dc7 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -22,7 +22,7 @@ "AllowMediaConversionHelp": "Garante ou nega acesso à funcionalidade de conversão de mídia.", "AllowOnTheFlySubtitleExtraction": "Permitir a extração da legenda em tempo real", "AllowOnTheFlySubtitleExtractionHelp": "Legendas incorporadas podem ser extraídas dos vídeos e entregues aos clientes como texto simples para ajudar a evitar a transcodificação do vídeo. Em alguns sistemas isto pode levar bastante tempo e causar travamento na reprodução do vídeo durante o processo de extração. Desative isto para ter as legendas incorporadas com a transcodificação do vídeo quando não forem nativamente suportadas pelo dispositivo cliente.", - "AllowRemoteAccess": "Permitir conexões remotas a este Servidor Jellyfin.", + "AllowRemoteAccess": "Permitir conexões remotas a este Servidor.", "AllowRemoteAccessHelp": "Se desmarcado, todas as conexões remotas serão bloqueadas.", "AllowedRemoteAddressesHelp": "Lista separada por vírgula de endereços IP ou entradas IP/netmask para redes que terão permissão para conectar-se remotamente. Se deixar em branco, todos os endereços remotos terão permissão.", "AlwaysPlaySubtitles": "Sempre reproduzir legendas", @@ -110,7 +110,7 @@ "ColorTransfer": "Transferência de cor", "CommunityRating": "Avaliação da comunidade", "Composer": "Compositor", - "ConfigureDateAdded": "Configure como a data de adição é determinada no painel do Servidor Jellyfin nas configurações da Biblioteca", + "ConfigureDateAdded": "Configure como a data de adição é determinada no painel nas configurações da Biblioteca", "ConfirmDeleteImage": "Excluir imagem?", "ConfirmDeleteItem": "Ao excluir este item, você o excluirá do sistema de arquivos e também da biblioteca de mídias. Deseja realmente continuar?", "ConfirmDeleteItems": "Ao excluir estes itens, você os excluirá do sistema de arquivos e de sua biblioteca de mídias. Deseja realmente continuar?", @@ -183,10 +183,10 @@ "EndsAtValue": "Termina às {0}", "Episodes": "Episódios", "ErrorAddingListingsToSchedulesDirect": "Ocorreu um erro ao adicionar a programação à sua conta da Schedules Direct. A Schedules Direct permite apenas um número limitado de programações por conta. Talvez seja necessário que você entre no website da Schedules Direct e remova outras listas de sua conta antes de prosseguir.", - "ErrorAddingMediaPathToVirtualFolder": "Um erro ocorreu ao adicionar o local da mídia. Por favor, verifique se o local é válido e se o processo do Servidor Jellyfin tem acesso a este local.", + "ErrorAddingMediaPathToVirtualFolder": "Um erro ocorreu ao adicionar o local da mídia. Por favor, verifique se o local é válido e se o Jellyfin tem acesso a este local.", "ErrorAddingTunerDevice": "Ocorreu um erro ao adicionar o sintonizador. Por favor, verifique se está acessível e tente novamente.", "ErrorAddingXmlTvFile": "Ocorreu um erro ao acessar o arquivo XmlTV. Por favor, verifique se o arquivo existe e tente novamente.", - "ErrorDeletingItem": "Ocorreu um erro ao excluir o item do Servidor Jellyfin. Por favor, verifique se o Servidor Jellyfin possui acesso de gravação na pasta de mídia e tente novamente.", + "ErrorDeletingItem": "Ocorreu um erro ao excluir o item do Servidor. Por favor, verifique se o Jellyfin possui acesso de gravação na pasta de mídia e tente novamente.", "ErrorGettingTvLineups": "Ocorreu um erro ao fazer download da programação de TV. Por favor, certifique-se que sua informação esteja correta e tente novamente.", "ErrorStartHourGreaterThanEnd": "A hora final deve ser maior que a hora inicial.", "ErrorPleaseSelectLineup": "Por favor, selecione a programação e tente novamente. Se não houver programações disponíveis, verifique se o seu nome de usuário, senha e código postal estão corretos.", @@ -449,7 +449,7 @@ "LabelAudioLanguagePreference": "Idioma preferido de áudio:", "LabelAutomaticallyRefreshInternetMetadataEvery": "Atualizar automaticamente os metadados da internet:", "LabelBindToLocalNetworkAddress": "Vincular a um endereço de rede local:", - "LabelBindToLocalNetworkAddressHelp": "Opcional. Sobrepor o endereço de IP local para vincular o servidor http. Se deixar em branco, o servidor será vinculado a todos os endereços disponíveis. Para alterar este valor é necessário reiniciar o Servidor Jellyfin.", + "LabelBindToLocalNetworkAddressHelp": "Opcional. Sobrepor o endereço de IP local para vincular o servidor http. Se deixar em branco, o servidor será vinculado a todos os endereços disponíveis. Para alterar este valor é necessário reiniciar.", "LabelBirthDate": "Data de nascimento:", "LabelBirthYear": "Ano de nascimento:", "LabelBlastMessageInterval": "Intervalo das mensagens ao vivo (segundos)", @@ -506,7 +506,7 @@ "LabelEnableBlastAliveMessages": "Mensagens ao vivo", "LabelEnableBlastAliveMessagesHelp": "Ative esta função se o servidor não for detectado por outros dispositivos UPnP em sua rede.", "LabelEnableDlnaClientDiscoveryInterval": "Intervalo para descoberta do cliente (segundos)", - "LabelEnableDlnaClientDiscoveryIntervalHelp": "Determina a duração em segundos entre buscas SSDP executadas pelo Jellyfin.", + "LabelEnableDlnaClientDiscoveryIntervalHelp": "Determina a duração em segundos entre buscas SSDP.", "LabelEnableDlnaDebugLogging": "Ativar o log de depuração de DLNA", "LabelEnableDlnaDebugLoggingHelp": "Cria arquivos de log grandes e só deve ser usado para resolver um problema.", "LabelEnableDlnaPlayTo": "Ativar DLNA Play To", @@ -541,7 +541,7 @@ "LabelHomeNetworkQuality": "Qualidade da rede local:", "LabelHomeScreenSectionValue": "Seção {0} da tela inicial:", "LabelHttpsPort": "Número da porta local de HTTPS:", - "LabelHttpsPortHelp": "O número da porta TCP que o servidor https do Jellyfin deveria se conectar.", + "LabelHttpsPortHelp": "O número da porta TCP do servidor HTTPS.", "LabelIconMaxHeight": "Altura máxima do ícone:", "LabelIconMaxHeightHelp": "Resolução máxima do ícone que é exposto via upnp:icon.", "LabelIconMaxWidth": "Largura máxima do ícone:", @@ -569,7 +569,7 @@ "LabelLanguage": "Idioma:", "LabelLineup": "Programação:", "LabelLocalHttpServerPortNumber": "Número da porta local de HTTP:", - "LabelLocalHttpServerPortNumberHelp": "O número da porta TCP que o servidor HTTP do Jellyfin deveria se conectar.", + "LabelLocalHttpServerPortNumberHelp": "O número da porta TCP para o servidor HTTP.", "LabelLockItemToPreventChanges": "Bloquear este item para evitar alterações futuras", "LabelLoginDisclaimer": "Aviso legal no login:", "LabelLoginDisclaimerHelp": "Um aviso será exibido na parte inferior da página de login.", @@ -594,7 +594,7 @@ "LabelMetadataReaders": "Leitores de metadados:", "LabelMetadataReadersHelp": "Classifica por ordem de prioridade suas fontes de metadados locais preferidas. O primeiro arquivo encontrado será lido.", "LabelMetadataSavers": "Gravadores de metadados:", - "LabelMetadataSaversHelp": "Escolha os formatos de arquivos nos quais deseja gravar seus metadados.", + "LabelMetadataSaversHelp": "Escolha os formatos a serem usados ao salvar seus metadados.", "LabelMethod": "Método:", "LabelMinBackdropDownloadWidth": "Tamanho mínimo da imagem de fundo para download:", "LabelMinResumeDuration": "Duração mínima para retomar:", @@ -610,7 +610,7 @@ "LabelMovieCategories": "Categorias de filmes:", "LabelMoviePrefix": "Prefixo dos filmes:", "LabelMoviePrefixHelp": "Se os títulos dos filmes devem ter um prefixo, digite-o aqui para que o servidor possa usá-lo corretamente.", - "LabelMovieRecordingPath": "Local de gravação de filme (opcional):", + "LabelMovieRecordingPath": "Local de gravação de filme:", "LabelMusicStreamingTranscodingBitrate": "Bitrate da transcodificação de músicas:", "LabelMusicStreamingTranscodingBitrateHelp": "Define o bitrate máximo do streaming de músicas.", "LabelName": "Nome:", @@ -622,8 +622,8 @@ "LabelNumber": "Número:", "LabelNumberOfGuideDays": "Número de dias de dados do guia para baixar:", "LabelNumberOfGuideDaysHelp": "Baixar mais dias do guia da TV permite agendar com maior antecedência e visualizar mais listas, mas também levará mais tempo para baixar. Se selecionar Automático, será escolhido o período baseado no número de canais.", - "LabelOptionalNetworkPath": "(Opcional) Pasta compartilhada em rede:", - "LabelOptionalNetworkPathHelp": "Se esta pasta estiver compartilhada em sua rede, informar o caminho do compartilhamento permitirá que os apps Jellyfin em outros dispositivos acessem arquivos de mídia diretamente. Por exemplo, {0} ou {1}.", + "LabelOptionalNetworkPath": "Pasta compartilhada em rede:", + "LabelOptionalNetworkPathHelp": "Se esta pasta estiver compartilhada em sua rede, informar o caminho do compartilhamento permitirá que os clientes em outros dispositivos acessem arquivos de mídia diretamente. Por exemplo, {0} ou {1}.", "LabelOriginalAspectRatio": "Proporção original da tela:", "LabelOriginalTitle": "Título original:", "LabelOverview": "Sinopse:", @@ -662,7 +662,7 @@ "LabelReleaseDate": "Data do lançamento:", "LabelRemoteClientBitrateLimit": "Limite do bitrate para streaming da internet (Mbps):", "LabelRemoteClientBitrateLimitHelp": "Um limite opcional da taxa de bits por-stream para todos os dispositivos fora da rede. Esta opção é útil para evitar que os dispositivos demandem uma taxa de bits maior que a permitida pela sua conexão. Isto pode causar um aumento na carga da CPU de seu servidor para que possa transcodificar os vídeos em tempo real para uma taxa mais baixa.", - "LabelRuntimeMinutes": "Duração (minutos):", + "LabelRuntimeMinutes": "Duração:", "LabelSaveLocalMetadata": "Salvar as artes nas pastas de mídia", "LabelSaveLocalMetadataHelp": "Salvando as artes nas pastas de mídia, permitirá deixá-las em um local fácil para editá-las.", "LabelScheduledTaskLastRan": "Última execução {0}, demorando {1}.", @@ -674,7 +674,7 @@ "LabelSelectVersionToInstall": "Selecione a versão para instalar:", "LabelSendNotificationToUsers": "Enviar notificação para:", "LabelSerialNumber": "Número de série", - "LabelSeriesRecordingPath": "Local de gravação de séries (opcional):", + "LabelSeriesRecordingPath": "Local de gravação de séries:", "LabelServerHost": "Servidor:", "LabelServerHostHelp": "192.168.1.100:8096 ou https://meuservidor.com", "LabelSimultaneousConnectionLimit": "Limite de stream simultâneo:", @@ -737,7 +737,7 @@ "LabelYoureDone": "Pronto!", "LabelZipCode": "CEP:", "LabelffmpegPath": "Local do FFmpeg:", - "LabelffmpegPathHelp": "O local para o programa ffmpeg, ou pasta contendo ffmpeg.", + "LabelffmpegPathHelp": "O local para o programa ffmpeg ou pasta contendo ffmpeg.", "LanNetworksHelp": "Lista separada por vírgula de endereços IP ou entradas IP/máscara de rede para redes que serão consideradas como redes locais ao forçar restrições de banda. Se definida, todos os outros endereços IP serão considerados como estando em uma rede externa e estarão sujeitos a restrições de banda externa. Se deixada em branco, apenas a sub-rede do servidor é considerada como rede local.", "Large": "Grande", "LatestFromLibrary": "{0} recentes", @@ -784,13 +784,13 @@ "MessageConfirmProfileDeletion": "Deseja realmente excluir este perfil?", "MessageConfirmRecordingCancellation": "Cancelar gravação?", "MessageConfirmRemoveMediaLocation": "Deseja realmente remover este local?", - "MessageConfirmRestart": "Deseja realmente reiniciar o Servidor Jellyfin?", - "MessageConfirmRevokeApiKey": "Deseja realmente revogar esta chave de api? A conexão da aplicação com o Servidor Jellyfin será abruptamente encerrada.", + "MessageConfirmRestart": "Deseja realmente reiniciar o Jellyfin?", + "MessageConfirmRevokeApiKey": "Deseja realmente revogar esta chave de API? A conexão da aplicação com este servidor será abruptamente encerrada.", "MessageConfirmShutdown": "Deseja realmente desligar o servidor ?", "MessageContactAdminToResetPassword": "Por favor, contate o administrador do sistema para redefinir sua senha.", "MessageCreateAccountAt": "Criar uma conta em {0}", "MessageDeleteTaskTrigger": "Deseja realmente excluir este disparador de tarefa?", - "MessageDirectoryPickerBSDInstruction": "Para BSD, você precisará configurar o armazenamento dentro de seu Jail do FreeNAS para permitir que o Jellyfin tenha acesso a ele.", + "MessageDirectoryPickerBSDInstruction": "Para BSD, você precisará configurar o armazenamento dentro de seu Jail do FreeNAS para que o Jellyfin tenha acesso a sua midia.", "MessageDirectoryPickerLinuxInstruction": "Sistemas operacionais Arch Linux, CentOS, Debian, Fedora, openSUSE ou Ubuntu, devem permitir que a conta de serviço tenha ao menos acesso de leitura nos locais de armazenamento.", "MessageDownloadQueued": "Download enfileirado.", "MessageEnablingOptionLongerScans": "Ativar esta opção pode resultar em rastreamentos de biblioteca significativamente mais demorados.", @@ -812,7 +812,7 @@ "MessagePleaseEnsureInternetMetadata": "Por favor, verifique se o download de metadados da internet está ativado.", "MessagePleaseWait": "Por favor, aguarde. Isto pode demorar um pouco.", "MessagePluginConfigurationRequiresLocalAccess": "Para configurar este plugin, por favor entre em seu servidor local diretamente.", - "MessagePluginInstallDisclaimer": "Plugins feitos por membros da comunidade Jellyfin são uma grande forma de melhorar sua experiência Jellyfin com funcionalidades e benefícios adicionais. Antes de instalar, por favor certifique-se de conhecer os efeitos que podem causar no seu Servidor Jellyfin, tais como rastreamentos de biblioteca mais demorados, processamento adicional e diminuição na estabilidade do sistema.", + "MessagePluginInstallDisclaimer": "Plugins feitos por membros da comunidade são uma grande forma de melhorar sua experiência com funcionalidades e benefícios adicionais. Antes de instalar, por favor certifique-se de conhecer os efeitos que podem causar no seu servidor, tais como rastreamentos de biblioteca mais demorados, processamento adicional e diminuição na estabilidade do sistema.", "MessageReenableUser": "Veja abaixo para reativar", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "As seguintes localizações de mídia serão excluídas de sua biblioteca:", "MessageUnableToConnectToServer": "Não foi possível conectar ao servidor selecionado. Por favor, verifique se está sendo executado e tente novamente.", @@ -820,7 +820,7 @@ "MessageYouHaveVersionInstalled": "Você possui a versão {0} instalada.", "Metadata": "Metadados", "MetadataManager": "Gerenciador de Metadados", - "MetadataSettingChangeHelp": "Alterar as configurações dos metadados afetará o novo conteúdo que será adicionado. Para atualizar o conteúdo existente, abra a tela de detalhes e clique no botão de atualizar, ou atualize usando o gerenciador de metadados.", + "MetadataSettingChangeHelp": "Alterar as configurações dos metadados afetará o novo conteúdo adicionado. Para atualizar o conteúdo existente, abra a tela de detalhes e clique no botão de atualizar, ou atualize usando o gerenciador de metadados.", "MinutesAfter": "minutos após", "MinutesBefore": "minutos antes", "Mobile": "Celular", @@ -867,7 +867,7 @@ "OptionAllowLinkSharingHelp": "Apenas páginas web que contenham informações de mídia são compartilhadas. Arquivos de mídia nunca são compartilhados publicamente. Os compartilhamentos têm um limite de tempo e expiram depois de {0} dias.", "OptionAllowManageLiveTv": "Permitir gerenciamento de gravações da TV ao Vivo", "OptionAllowMediaPlayback": "Permitir reprodução de mídia", - "OptionAllowMediaPlaybackTranscodingHelp": "Restringir o acesso à transcodificação pode ocasionar falhas na reprodução nos apps do Jellyfin devido a formatos de mídias não suportados.", + "OptionAllowMediaPlaybackTranscodingHelp": "Restringir o acesso à transcodificação pode ocasionar falhas na reprodução nos clientes devido a formatos de mídias não suportados.", "OptionAllowRemoteControlOthers": "Permitir controle remoto de outros usuários", "OptionAllowRemoteSharedDevices": "Permitir controle remoto de dispositivos compartilhados", "OptionAllowRemoteSharedDevicesHelp": "Dispositivos DLNA são autorizados até que um usuário altere as permissões.", @@ -878,7 +878,7 @@ "OptionArtist": "Artista", "OptionAscending": "Crescente", "OptionAutomaticallyGroupSeries": "Mesclar automaticamente séries que estão em várias pastas", - "OptionAutomaticallyGroupSeriesHelp": "Se ativado, séries que estiverem em várias pastas dentro desta biblioteca serão automaticamente mescladas em uma única série.", + "OptionAutomaticallyGroupSeriesHelp": "Séries que estiverem em várias pastas dentro desta biblioteca serão automaticamente mescladas em uma única série.", "OptionBlockBooks": "Livros", "OptionBlockChannelContent": "Conteúdo do Canal de Internet", "OptionBlockLiveTvChannels": "Canais de TV ao Vivo", @@ -896,7 +896,7 @@ "OptionDatePlayed": "Data de Reprodução", "OptionDescending": "Decrescente", "OptionDisableUser": "Desativar este usuário", - "OptionDisableUserHelp": "Se desativado, o servidor não permitirá nenhuma conexão deste usuário. Conexões existentes serão encerradas imediatamente.", + "OptionDisableUserHelp": "O servidor não permitirá nenhuma conexão deste usuário. Conexões existentes serão encerradas imediatamente.", "OptionDislikes": "Não gostei", "OptionDisplayFolderView": "Exibe visualização em pastas para exibir pastas de mídias", "OptionDisplayFolderViewHelp": "Exibição em pastas ao lado das biblioteca de mídia. Isto pode ser útil para visualizar por pastas.", @@ -905,7 +905,7 @@ "OptionDownloadBoxImage": "Caixa", "OptionDownloadDiscImage": "Disco", "OptionDownloadImagesInAdvance": "Fazer download de imagens antecipadamente", - "OptionDownloadImagesInAdvanceHelp": "Por padrão, a maioria das imagens são baixadas somente quando um app Jellyfin solicita. Ativar esta opção, baixará todas as imagens antecipadamente, ao importar novas mídias. Isto pode ocasionar um tempo maior para escanear a biblioteca.", + "OptionDownloadImagesInAdvanceHelp": "Por padrão, a maioria das imagens são baixadas somente quando um cliente solicita. Ativar esta opção, baixará todas as imagens antecipadamente, ao importar novas mídias. Isto pode ocasionar um tempo maior para escanear a biblioteca.", "OptionDownloadPrimaryImage": "Principal", "OptionDownloadThumbImage": "Miniatura", "OptionDvd": "DVD", @@ -934,7 +934,7 @@ "OptionHlsSegmentedSubtitles": "Legendas segmentadas HLS", "OptionHomeVideos": "Fotos", "OptionIgnoreTranscodeByteRangeRequests": "Ignorar requisições de extensão do byte de transcodificação", - "OptionIgnoreTranscodeByteRangeRequestsHelp": "Se ativado, estas requisições serão honradas mas irão ignorar o cabeçalho da extensão do byte.", + "OptionIgnoreTranscodeByteRangeRequestsHelp": "Estas requisições serão honradas mas irão ignorar o cabeçalho da extensão do byte.", "OptionImdbRating": "Avaliação IMDb", "OptionLikes": "Curtidas", "OptionMax": "Máx", @@ -945,9 +945,9 @@ "OptionOnInterval": "Em um intervalo", "OptionParentalRating": "Classificação Etária", "OptionPlainStorageFolders": "Exibir todas as pastas como pastas de armazenamento", - "OptionPlainStorageFoldersHelp": "Se ativado, todas as pastas são representadas no DIDL como \"object.container.storageFolder\" ao invés de um tipo mais específico como, por exemplo, \"object.container.person.musicArtist\".", + "OptionPlainStorageFoldersHelp": "Todas as pastas são representadas no DIDL como \"object.container.storageFolder\" ao invés de um tipo mais específico como, por exemplo, \"object.container.person.musicArtist\".", "OptionPlainVideoItems": "Exibir todos os vídeos como itens de vídeo", - "OptionPlainVideoItemsHelp": "Se ativado, todos os vídeos são representados no DIDL como \"object.item.videoItem\" ao invés de um tipo mais específico como, por exemplo, \"object.item.videoItem.movie\".", + "OptionPlainVideoItemsHelp": "Todos os vídeos são representados no DIDL como \"object.item.videoItem\" ao invés de um tipo mais específico como, por exemplo, \"object.item.videoItem.movie\".", "OptionPlayCount": "Contagem de Reproduções", "OptionPlayed": "Reproduzido", "OptionPremiereDate": "Data da Estréia", @@ -964,7 +964,7 @@ "OptionResumable": "Retomável", "OptionRuntime": "Duração", "OptionSaveMetadataAsHidden": "Salvar metadados e imagens como arquivos ocultos", - "OptionSaveMetadataAsHiddenHelp": "Isto será aplicado sobre novos metadados salvos. Os arquivos de metadados existentes serão atualizados na próxima vez que forem salvos no Servidor Jellyfin.", + "OptionSaveMetadataAsHiddenHelp": "Isto será aplicado sobre novos metadados salvos. Os arquivos de metadados existentes serão atualizados na próxima vez que forem salvos no servidor.", "OptionSpecialEpisode": "Especiais", "OptionTrackName": "Nome da Faixa", "OptionTvdbRating": "Avaliação TVDB", @@ -1002,9 +1002,9 @@ "PleaseAddAtLeastOneFolder": "Por favor, adicione ao menos uma pasta a esta biblioteca, clicando no botão Adicionar.", "PleaseConfirmPluginInstallation": "Por favor, clique em OK para confirmar que você leu e deseja prosseguir com a instalação do plugin.", "PleaseEnterNameOrId": "Por favor, digite um nome ou ID externa.", - "PleaseRestartServerName": "Por favor, reinicie o Servidor Jellyfin - {0}.", + "PleaseRestartServerName": "Por favor, reinicie o Jellyfin {0}.", "PleaseSelectTwoItems": "Por favor, selecione pelo menos dois itens.", - "MessagePluginInstalled": "O plugin foi instalado com sucesso. O Servidor Jellyfin precisa ser reiniciado para que as alterações sejam aplicadas.", + "MessagePluginInstalled": "O plugin foi instalado com sucesso. O servidor precisa ser reiniciado para que as alterações sejam aplicadas.", "PreferEmbeddedTitlesOverFileNames": "Preferir títulos incorporados ao invés de nomes de arquivos", "PreferEmbeddedTitlesOverFileNamesHelp": "Isto determina a exibição padrão do título quando não houverem metadados da internet ou locais disponíveis.", "Premieres": "Estreias", @@ -1028,7 +1028,7 @@ "RecordingScheduled": "Gravação agendada.", "Recordings": "Gravações", "Refresh": "Atualizar", - "RefreshDialogHelp": "Os metadados são atualizados com base nas configurações e nos serviços de internet que estão ativados no painel do Servidor Jellyfin.", + "RefreshDialogHelp": "Os metadados são atualizados com base nas configurações e nos serviços de internet que estão ativados no painel.", "RefreshMetadata": "Atualizar metadados", "RefreshQueued": "Atualização enfileirada.", "ReleaseDate": "Data de lançamento", @@ -1065,10 +1065,10 @@ "SeriesRecordingScheduled": "Gravação de série agendada.", "SeriesSettings": "Configurações de Séries", "SeriesYearToPresent": "{0} - Presente", - "ServerNameIsRestarting": "Servidor Jellyfin - {0} está reiniciando.", - "ServerNameIsShuttingDown": "Servidor Jellyfin - {0} está desligando.", - "ServerRestartNeededAfterPluginInstall": "O Servidor Jellyfin precisa ser reiniciado após a instalação de um plugin.", - "ServerUpdateNeeded": "Este Servidor Jellyfin precisa ser atualizado. Para fazer download da versão mais recente, por favor visite {0}", + "ServerNameIsRestarting": "O servidor em {0} está reiniciando.", + "ServerNameIsShuttingDown": "O servidor em {0} está desligando.", + "ServerRestartNeededAfterPluginInstall": "O Jellyfin precisa ser reiniciado após a instalação de um plugin.", + "ServerUpdateNeeded": "Este servidor precisa ser atualizado. Para fazer download da versão mais recente, por favor visite {0}", "Settings": "Configurações", "SettingsSaved": "Configurações salvas.", "SettingsWarning": "Alterar estes valores pode causar instabilidade ou falhas de conectividade. Se tiver algum problema, recomendamos retornar ao padrão.", @@ -1269,7 +1269,7 @@ "OptionSubstring": "Substring", "OptionThumb": "Miniatura", "OptionThumbCard": "Cartão de miniatura", - "PasswordResetProviderHelp": "Escolha um fornecedor de redefinição de senhas para ser usado quando este usuário solicitar a redefinição de senha", + "PasswordResetProviderHelp": "Escolha um provedor de reinício de senha a ser usado quando este usuário solicitar uma redefinição de senha.", "PictureInPicture": "Picture in picture", "PlaybackData": "Dados de Reprodução", "Premiere": "Estreia", @@ -1344,7 +1344,7 @@ "LastSeen": "Visto pela última vez {0}", "PersonRole": "como {0}", "ListPaging": "{0}-{1} de {2}", - "WriteAccessRequired": "O servidor Jellyfin necessita de acesso de escrita para essa pasta. Garanta o acesso e tente novamente.", + "WriteAccessRequired": "O Jellyfin necessita de acesso de escrita para essa pasta. Garanta o acesso e tente novamente.", "PathNotFound": "O caminho não pôde ser encontrado. Por favor certifique-se da validade e tente novamente.", "YadifBob": "YADIF Bob", "Yadif": "YADIF", @@ -1436,5 +1436,9 @@ "ViewAlbumArtist": "Ver artista do álbum", "PreviousTrack": "Ir para o anterior", "NextTrack": "Ir para o próximo", - "LabelUnstable": "Instável" + "LabelUnstable": "Instável", + "Preview": "Pré-visualização", + "LabelSubtitleVerticalPosition": "Posição Vertical:", + "MessageGetInstalledPluginsError": "Um erro ocorreu durante a requisição da lista de plugins atualmente instalados.", + "MessagePluginInstallError": "Um erro ocorreu durante a instalação do plugin." } From e3f95753992a04c4bde01e0b7d30686c43ad2b92 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Wed, 26 Aug 2020 22:13:45 +0000 Subject: [PATCH 17/31] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/ --- src/strings/sl-si.json | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index e4cc58de6d..641d21e8bc 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -526,7 +526,7 @@ "LabelCriticRating": "Ocena kritikov:", "LabelCustomCertificatePathHelp": "Pot do PKCS #12 datoteke, ki vsebuje certifikat in zasebni ključ, za omogočanje TLS povezave na domenah po meri.", "LabelCustomCss": "CSS po meri:", - "LabelCustomCssHelp": "Določite vaš lasten slog spletnega vmesnika.", + "LabelCustomCssHelp": "Uveljavite vaš lasten slog spletnega vmesnika.", "LabelCustomDeviceDisplayNameHelp": "Določi prikazano ime naprave. Pusti prazno za uporabo imena kot ga sporoči naprava sama.", "LabelDefaultScreen": "Privzeti zaslon:", "LabelDateAdded": "Datum dodajanja:", @@ -681,7 +681,7 @@ "Small": "Majhno", "SmartSubtitlesHelp": "Podnapisi, ki se ujemajo z želenim jezikom, bodo naloženi, ko je zvok v tujem jeziku.", "SubtitleAppearanceSettingsDisclaimer": "Te nastavitve ne vplivajo na grafične podnapise (PGS, DVD, itd.) ali ASS/SSA podnapise, ki imajo vdelan svoj lasten slog.", - "MessagePluginInstalled": "Dodatek je bil uspešno nameščen. Za uveljavitev sprememb je potreben ponovni zagon Jellyfin strežnika.", + "MessagePluginInstalled": "Dodatek je bil uspešno nameščen. Za uveljavitev sprememb je potreben ponovni zagon strežnika.", "MessageNoMovieSuggestionsAvailable": "Trenutno ni na voljo nobenih predlogov za filme. Začnite gledati in ocenjevati vaše filme, ter se nato vrnite sem in si oglejte predloge.", "LabelSelectFolderGroups": "Samodejno združi vsebine iz spodnjih map v poglede kot so Filmi, Glasba in TV:", "TitlePlayback": "Predvajanje", @@ -751,7 +751,7 @@ "Quality": "Kvaliteta", "PlaceFavoriteChannelsAtBeginning": "Postavi priljubljene kanale na začetek", "LabelOptionalNetworkPath": "Omrežna mapa v skupni rabi:", - "LabelOptionalNetworkPathHelp": "V primeru, da je mapa deljena v vašem omrežju, lahko Jellyfin deli omrežno pot z ostalimi napravami in jim omogoči neposreden dostop do vsebin. Na primer {0} ali {1}.", + "LabelOptionalNetworkPathHelp": "V primeru, da je ta mapa deljena v vašem omrežju, lahko Jellyfin deli omrežno pot z ostalimi napravami in jim omogoči neposreden dostop do vsebin. Na primer {0} ali {1}.", "LabelRemoteClientBitrateLimitHelp": "Neobvezna omejitev bitne hitrosti na posamezno predvajanje za vse naprave izven domačega omrežja. S tem lahko preprečite, da bi naprave zahtevale višjo bitno hitrost predvajanja, kot jo lahko prenese vaše omrežje. To lahko poveča obremenitev CPU-ja, saj bo morda potrebno sprotno prekodiranje za zmanjšanje bitne hitrosti.", "LanNetworksHelp": "Z vejico ločen seznam IP naslovov ali IP/maska omrežji, ki bodo upoštevana kot lokalna pri uveljavljanju omejitev pasovne širine. Če nastavite, se bodo vsi ostali naslovi upoštevali kot zunanji in bodo predmet omejitve pasovne širine. Če pustite prazno, bo kot lokalno omrežje upoštevano zgolj omrežje strežnika.", "MessageForgotPasswordInNetworkRequired": "Za začetek ponastavitve gesla prosimo poskusite znova v vašem domačem omrežju.", @@ -1342,5 +1342,18 @@ "SubtitleOffset": "Zamik podnapisev", "Subtitles": "Podnapisi", "Sunday": "Nedelja", - "TabAdvanced": "Napredno" + "TabAdvanced": "Napredno", + "MessageChangeRecordingPath": "Spreminjanje mape posnetkov ne bo premaknilo obstoječih posnetkov iz stare na novo lokacijo. Če želite, jih morate premakniti ročno.", + "RecordingCancelled": "Snemanje preklicano.", + "RecordSeries": "Snemaj serijo", + "Record": "Snemaj", + "RecommendationBecauseYouWatched": "Ker ste gledali {0}", + "Primary": "Primarna", + "PreviousTrack": "Preskoči na prejšnjo", + "PreferEmbeddedEpisodeInfosOverFileNames": "Prioritiziraj vdelane podatke o epizodi pred imenom datoteke", + "PreferEmbeddedEpisodeInfosOverFileNamesHelp": "Uporabi podatke o epizodi iz vdelanih metapodatkov, če so na voljo.", + "MessageGetInstalledPluginsError": "Pri pridobivanju seznama trenutno nameščenih dodatkov je prišlo do napake.", + "MessagePluginInstallError": "Pri nameščanju dodatka je prišlo do napake.", + "PlaybackErrorNoCompatibleStream": "Ta odjemalec ni kompatibilen s predstavnostjo in strežnik ne pošilja kompatibilnega formata predstavnosti.", + "PlaybackRate": "Hitrost predvajanja" } From 795583e346de9b4e9a836569b03aa12ae2fabb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Thu, 27 Aug 2020 01:20:18 +0000 Subject: [PATCH 18/31] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/ --- src/strings/pt-br.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index c5e25e7dc7..5299a5aebc 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -1440,5 +1440,6 @@ "Preview": "Pré-visualização", "LabelSubtitleVerticalPosition": "Posição Vertical:", "MessageGetInstalledPluginsError": "Um erro ocorreu durante a requisição da lista de plugins atualmente instalados.", - "MessagePluginInstallError": "Um erro ocorreu durante a instalação do plugin." + "MessagePluginInstallError": "Um erro ocorreu durante a instalação do plugin.", + "PlaybackRate": "Taxa de Reprodução" } From 9edfc3e5dccb6ec9fed56c8d4f1c50efd5d3f47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Thu, 27 Aug 2020 01:22:02 +0000 Subject: [PATCH 19/31] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/ --- src/strings/pt-br.json | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index 5299a5aebc..381d532aaf 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -657,7 +657,7 @@ "LabelReasonForTranscoding": "Motivo da transcodificação:", "LabelRecord": "Gravar:", "LabelRecordingPath": "Local de gravação padrão:", - "LabelRecordingPathHelp": "Define o local padrão para salvar as gravações. Se deixar não preenchido, a pasta de dados do programa do servidor será usada.", + "LabelRecordingPathHelp": "Defina o local padrão para salvar as gravações. Se deixar em branco, a pasta de dados do programa do servidor será usada.", "LabelRefreshMode": "Modo de atualização:", "LabelReleaseDate": "Data do lançamento:", "LabelRemoteClientBitrateLimit": "Limite do bitrate para streaming da internet (Mbps):", @@ -678,8 +678,8 @@ "LabelServerHost": "Servidor:", "LabelServerHostHelp": "192.168.1.100:8096 ou https://meuservidor.com", "LabelSimultaneousConnectionLimit": "Limite de stream simultâneo:", - "LabelSkipBackLength": "Tamanho do intervalo para retroceder:", - "LabelSkipForwardLength": "Tamanho do intervalo para avançar:", + "LabelSkipBackLength": "Intervalo ao retroceder:", + "LabelSkipForwardLength": "Intervalo ao avançar:", "LabelSkipIfAudioTrackPresent": "Ignorar se a faixa de áudio padrão coincidir com o idioma baixado", "LabelSkipIfAudioTrackPresentHelp": "Desmarque esta opção para que todos os vídeos tenham legendas, independente do idioma do áudio.", "LabelSkipIfGraphicalSubsPresent": "Ignorar se o vídeo já possuir legendas incorporadas", @@ -699,7 +699,7 @@ "LabelSubtitleFormatHelp": "Exemplo: srt", "LabelSubtitlePlaybackMode": "Modo de legenda:", "LabelSupportedMediaTypes": "Tipos de Mídia Suportados:", - "LabelTVHomeScreen": "Tela inicial do modo TV:", + "LabelTVHomeScreen": "Tela de início do modo TV:", "LabelTagline": "Slogan:", "LabelTextBackgroundColor": "Cor de fundo do texto:", "LabelTextColor": "Cor do texto:", @@ -737,7 +737,7 @@ "LabelYoureDone": "Pronto!", "LabelZipCode": "CEP:", "LabelffmpegPath": "Local do FFmpeg:", - "LabelffmpegPathHelp": "O local para o programa ffmpeg ou pasta contendo ffmpeg.", + "LabelffmpegPathHelp": "O local para o arquivo de aplicação ffmpeg, ou pasta contendo ffmpeg.", "LanNetworksHelp": "Lista separada por vírgula de endereços IP ou entradas IP/máscara de rede para redes que serão consideradas como redes locais ao forçar restrições de banda. Se definida, todos os outros endereços IP serão considerados como estando em uma rede externa e estarão sujeitos a restrições de banda externa. Se deixada em branco, apenas a sub-rede do servidor é considerada como rede local.", "Large": "Grande", "LatestFromLibrary": "{0} recentes", @@ -791,7 +791,7 @@ "MessageCreateAccountAt": "Criar uma conta em {0}", "MessageDeleteTaskTrigger": "Deseja realmente excluir este disparador de tarefa?", "MessageDirectoryPickerBSDInstruction": "Para BSD, você precisará configurar o armazenamento dentro de seu Jail do FreeNAS para que o Jellyfin tenha acesso a sua midia.", - "MessageDirectoryPickerLinuxInstruction": "Sistemas operacionais Arch Linux, CentOS, Debian, Fedora, openSUSE ou Ubuntu, devem permitir que a conta de serviço tenha ao menos acesso de leitura nos locais de armazenamento.", + "MessageDirectoryPickerLinuxInstruction": "Para Linux no Arch Linux, CentOS, Debian, Fedora, openSUSE ou Ubuntu, você deve permitir que o usuário do serviço tenha ao menos acesso de leitura ao seu armazenamento.", "MessageDownloadQueued": "Download enfileirado.", "MessageEnablingOptionLongerScans": "Ativar esta opção pode resultar em rastreamentos de biblioteca significativamente mais demorados.", "MessageFileReadError": "Ocorreu um erro ao ler o arquivo. Por favor, tente novamente.", @@ -820,7 +820,7 @@ "MessageYouHaveVersionInstalled": "Você possui a versão {0} instalada.", "Metadata": "Metadados", "MetadataManager": "Gerenciador de Metadados", - "MetadataSettingChangeHelp": "Alterar as configurações dos metadados afetará o novo conteúdo adicionado. Para atualizar o conteúdo existente, abra a tela de detalhes e clique no botão de atualizar, ou atualize usando o gerenciador de metadados.", + "MetadataSettingChangeHelp": "Alterar as configurações dos metadados afetará o novo conteúdo que será adicionado. Para atualizar o conteúdo existente, abra a tela de detalhes e clique no botão de atualizar ou atualize usando o gerenciador de metadados.", "MinutesAfter": "minutos após", "MinutesBefore": "minutos antes", "Mobile": "Celular", @@ -870,10 +870,10 @@ "OptionAllowMediaPlaybackTranscodingHelp": "Restringir o acesso à transcodificação pode ocasionar falhas na reprodução nos clientes devido a formatos de mídias não suportados.", "OptionAllowRemoteControlOthers": "Permitir controle remoto de outros usuários", "OptionAllowRemoteSharedDevices": "Permitir controle remoto de dispositivos compartilhados", - "OptionAllowRemoteSharedDevicesHelp": "Dispositivos DLNA são autorizados até que um usuário altere as permissões.", + "OptionAllowRemoteSharedDevicesHelp": "Dispositivos DLNA são considerados compartilhados até que um usuário comece a controlá-los.", "OptionAllowSyncTranscoding": "Permitir download e sincronização de mídia que necessite de transcodificação", "OptionAllowUserToManageServer": "Permitir este usuário administrar o servidor", - "OptionAllowVideoPlaybackRemuxing": "Permitir reprodução de vídeos que requerem conversão sem recodificar", + "OptionAllowVideoPlaybackRemuxing": "Permitir reprodução de vídeos que requeiram conversão sem re-encodação", "OptionAllowVideoPlaybackTranscoding": "Permitir reprodução de vídeo que necessite de transcodificação", "OptionArtist": "Artista", "OptionAscending": "Crescente", @@ -897,9 +897,9 @@ "OptionDescending": "Decrescente", "OptionDisableUser": "Desativar este usuário", "OptionDisableUserHelp": "O servidor não permitirá nenhuma conexão deste usuário. Conexões existentes serão encerradas imediatamente.", - "OptionDislikes": "Não gostei", - "OptionDisplayFolderView": "Exibe visualização em pastas para exibir pastas de mídias", - "OptionDisplayFolderViewHelp": "Exibição em pastas ao lado das biblioteca de mídia. Isto pode ser útil para visualizar por pastas.", + "OptionDislikes": "Não Curtidos", + "OptionDisplayFolderView": "Exibe uma visualização de pasta para exibir pastas de mídias", + "OptionDisplayFolderViewHelp": "Exibe pastas ao lado de suas outras biblioteca de mídia. Isto pode ser útil se quiser uma visualização por pasta.", "OptionDownloadArtImage": "Arte", "OptionDownloadBackImage": "Traseira", "OptionDownloadBoxImage": "Caixa", @@ -1226,9 +1226,9 @@ "HeaderFavoriteVideos": "Videos favoritos", "LabelAuthProvider": "Provedor de autenticação:", "LabelServerName": "Nome do servidor:", - "LabelTranscodePath": "Pasta de transcodificação:", - "LabelTranscodes": "Transcodificação:", - "LabelUserLoginAttemptsBeforeLockout": "Tentativas de login com falha antes que o usuário seja bloqueado:", + "LabelTranscodePath": "Caminho para a transcodificação:", + "LabelTranscodes": "Transcodificações:", + "LabelUserLoginAttemptsBeforeLockout": "Número de tentativas de login antes de bloquear o usuário:", "DashboardVersionNumber": "Versão: {0}", "DashboardServerName": "Servidor: {0}", "DashboardOperatingSystem": "Sistema Operacional: {0}", @@ -1410,7 +1410,7 @@ "HeaderSyncPlaySelectGroup": "Entrar em um grupo", "EnableDetailsBanner": "Banner de detalhes", "EnableDetailsBannerHelp": "Exibe um banner na parte superior da página de detalhes do item.", - "EnableBlurHashHelp": "Imagens que ainda estão carregando vão mostrar em seu lugar representações", + "EnableBlurHashHelp": "Imagens que ainda estão carregando vão mostrar em seu lugar representações.", "EnableBlurHash": "Habilitar efeito borrado para imagens previas", "ShowMore": "Mostrar mais", "ShowLess": "Mostrar menos", @@ -1441,5 +1441,6 @@ "LabelSubtitleVerticalPosition": "Posição Vertical:", "MessageGetInstalledPluginsError": "Um erro ocorreu durante a requisição da lista de plugins atualmente instalados.", "MessagePluginInstallError": "Um erro ocorreu durante a instalação do plugin.", - "PlaybackRate": "Taxa de Reprodução" + "PlaybackRate": "Taxa de Reprodução", + "SubtitleVerticalPositionHelp": "Numero da linha onde o texto aparece. Números positivos indicam de cima para baixo. Números negativos indicam de baixo para cima." } From 8b91d7482bdd20fc9665a5d8cbf6493710dfdd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Thu, 27 Aug 2020 01:40:23 +0000 Subject: [PATCH 20/31] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/ --- src/strings/pt-br.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index 381d532aaf..e6be09c100 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -17,12 +17,12 @@ "AllEpisodes": "Todos os episódios", "AllLanguages": "Todos os idiomas", "AllLibraries": "Todas as bibliotecas", - "AllowHWTranscodingHelp": "Se ativado, permite ao sintonizador transcodificar streams em tempo real. Isto pode ajudar a reduzir a transcodificação requerida pelo Servidor.", + "AllowHWTranscodingHelp": "Permite ao sintonizador transcodificar streams em tempo real. Isto pode ajudar a reduzir a transcodificação requerida pelo servidor.", "AllowMediaConversion": "Permitir conversão de mídia", "AllowMediaConversionHelp": "Garante ou nega acesso à funcionalidade de conversão de mídia.", "AllowOnTheFlySubtitleExtraction": "Permitir a extração da legenda em tempo real", "AllowOnTheFlySubtitleExtractionHelp": "Legendas incorporadas podem ser extraídas dos vídeos e entregues aos clientes como texto simples para ajudar a evitar a transcodificação do vídeo. Em alguns sistemas isto pode levar bastante tempo e causar travamento na reprodução do vídeo durante o processo de extração. Desative isto para ter as legendas incorporadas com a transcodificação do vídeo quando não forem nativamente suportadas pelo dispositivo cliente.", - "AllowRemoteAccess": "Permitir conexões remotas a este Servidor.", + "AllowRemoteAccess": "Permitir conexões remotas a este servidor.", "AllowRemoteAccessHelp": "Se desmarcado, todas as conexões remotas serão bloqueadas.", "AllowedRemoteAddressesHelp": "Lista separada por vírgula de endereços IP ou entradas IP/netmask para redes que terão permissão para conectar-se remotamente. Se deixar em branco, todos os endereços remotos terão permissão.", "AlwaysPlaySubtitles": "Sempre reproduzir legendas", @@ -657,7 +657,7 @@ "LabelReasonForTranscoding": "Motivo da transcodificação:", "LabelRecord": "Gravar:", "LabelRecordingPath": "Local de gravação padrão:", - "LabelRecordingPathHelp": "Defina o local padrão para salvar as gravações. Se deixar em branco, a pasta de dados do programa do servidor será usada.", + "LabelRecordingPathHelp": "Define o local padrão para salvar as gravações. Se deixar em branco, a pasta de dados do programa do servidor será usada.", "LabelRefreshMode": "Modo de atualização:", "LabelReleaseDate": "Data do lançamento:", "LabelRemoteClientBitrateLimit": "Limite do bitrate para streaming da internet (Mbps):", @@ -1359,8 +1359,8 @@ "DeinterlaceMethodHelp": "Selecione o método de desentrelaçamento a ser usado ao transcodificar o conteúdo entrelaçado.", "BoxSet": "Coleção", "Artist": "Artista", - "AlbumArtist": "Artista do Album", - "Album": "Album", + "AlbumArtist": "Artista do Álbum", + "Album": "Álbum", "UnsupportedPlayback": "O Jellyfin não pode descriptografar conteúdo protegido por DRM, porém mesmo assim fará uma tentativa para todo tipo de conteúdo, incluindo títulos protegidos. A imagem de alguns arquivos pode aparecer completamente preta devido a criptografia ou outros recursos não suportados, como títulos interativos.", "ButtonTogglePlaylist": "Playlist", "Filter": "Filtro", From d79d259109200dde00e22d9fef6d7e6312da4aad Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:53:57 +0000 Subject: [PATCH 21/31] Bump hls.js from 0.14.9 to 0.14.10 Bumps [hls.js](https://github.com/video-dev/hls.js) from 0.14.9 to 0.14.10. - [Release notes](https://github.com/video-dev/hls.js/releases) - [Changelog](https://github.com/video-dev/hls.js/blob/master/docs/release-process.md) - [Commits](https://github.com/video-dev/hls.js/compare/v0.14.9...v0.14.10) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d6e8c1941f..203be0749d 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "fast-text-encoding": "^1.0.3", "flv.js": "^1.5.0", "headroom.js": "^0.11.0", - "hls.js": "^0.14.9", + "hls.js": "^0.14.10", "howler": "^2.2.0", "intersection-observer": "^0.11.0", "jellyfin-apiclient": "^1.4.1", diff --git a/yarn.lock b/yarn.lock index 5e93eef199..86c167aee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5459,10 +5459,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -hls.js@^0.14.9: - version "0.14.9" - resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-0.14.9.tgz#e85be87d94385ed9947155716578f7c568957d15" - integrity sha512-5j1ONTvIzcIxCtg2eafikFbZ3b/9fDhR6u871LmK7jZ44/Qdc2G4xaSBCwcVK61gz7kTyiobaAhB++2M4J58rQ== +hls.js@^0.14.10: + version "0.14.10" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-0.14.10.tgz#51365ecc2764066e6b4379587aee64c1e181f683" + integrity sha512-muBt5Gxhapgk2uV8aEKRYe9GJ6AI5niFfDuOd8ZXHga519RNJ0+QAeRPdEpl1QKMqj1lT/6r/WKVnLL+ePB6ow== dependencies: eventemitter3 "^4.0.3" url-toolkit "^2.1.6" From c88bf99cc14a01fadb22a21fe7316905c117d003 Mon Sep 17 00:00:00 2001 From: Hilman Maulana Date: Fri, 28 Aug 2020 03:02:00 +0000 Subject: [PATCH 22/31] Translated using Weblate (Indonesian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/ --- src/strings/id.json | 98 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/src/strings/id.json b/src/strings/id.json index fed8694ca2..a4e0031094 100644 --- a/src/strings/id.json +++ b/src/strings/id.json @@ -31,7 +31,7 @@ "Albums": "Album", "Books": "Buku", "Favorites": "Favorit", - "Genres": "Genre", + "Genres": "Aliran", "HeaderFavoriteSongs": "Lagu Favorit", "HeaderFavoriteAlbums": "Album Favorit", "HeaderFavoriteArtists": "Artis Favorit", @@ -298,5 +298,99 @@ "HeaderProfileServerSettingsHelp": "Nilai-nilai ini mengontrol bagaimana server akan menampilkan dirinya kepada klien.", "HeaderConnectToServer": "Sambungkan ke server", "HeaderApiKeysHelp": "Aplikasi eksternal diharuskan memiliki kunci API untuk berkomunikasi dengan server. Kunci dikeluarkan dengan masuk dengan akun pengguna biasa atau secara manual memberi aplikasi kunci.", - "ErrorDeletingItem": "Terjadi kesalahan saat menghapus item dari server. Harap periksa apakah Jellyfin memiliki akses tulis ke folder media dan coba lagi." + "ErrorDeletingItem": "Terjadi kesalahan saat menghapus item dari server. Harap periksa apakah Jellyfin memiliki akses tulis ke folder media dan coba lagi.", + "LabelHttpsPort": "Nomor port HTTPS lokal :", + "LabelHomeScreenSectionValue": "Bagian layar beranda {0} :", + "LabelHomeNetworkQuality": "Kualitas jaringan rumah :", + "LabelHardwareAccelerationTypeHelp": "Akselerasi perangkat keras membutuhkan konfigurasi tambahan.", + "LabelHardwareAccelerationType": "Akselerasi perangkat keras :", + "LabelEncoderPreset": "Preset pengkodean H264 dan H265 :", + "LabelH264Crf": "H264 pengkodean CRF :", + "LabelGroupMoviesIntoCollectionsHelp": "Saat menampilkan daftar film, film dalam sebuah koleksi akan ditampilkan sebagai satu item yang dikelompokkan.", + "LabelGroupMoviesIntoCollections": "Kelompokkan film ke dalam koleksi", + "LabelFriendlyName": "Nama ramah :", + "LabelFormat": "Format :", + "LabelForgotPasswordUsernameHelp": "Masukkan nama pengguna Anda, jika Anda mengingatnya.", + "LabelFont": "Font :", + "LabelFolder": "Folder :", + "LabelFileOrUrl": "File atau URL :", + "LabelFailed": "Gagal", + "HeaderChannelAccess": "Akses Saluran", + "HeaderCastAndCrew": "Pemeran & Kru", + "HeaderCancelSeries": "Batalkan Serial", + "HeaderCancelRecording": "Batalkan Perekaman", + "HeaderBranding": "Branding", + "HeaderBlockItemsWithNoRating": "Blokir item dengan informasi peringkat yang tidak ada atau tidak dikenal :", + "HeaderAudioSettings": "Pengaturan Audio", + "HeaderAudioBooks": "Buku Audio", + "HeaderAppearsOn": "Muncul Di", + "HeaderApp": "App", + "ApiKeysCaption": "Daftar API keys yang saat ini diaktifkan", + "HeaderApiKeys": "API Keys", + "HeaderApiKey": "API Key", + "HeaderAllowMediaDeletionFrom": "Izinkan Penghapusan Media Dari", + "HeaderAlert": "Peringatan", + "HeaderAdmin": "Admin", + "HeaderAdditionalParts": "Bagian Tambahan", + "HeaderAddUpdateImage": "Tambahkan / Perbarui Gambar", + "HeaderAddToPlaylist": "Tambahkan ke Daftar Putar", + "HeaderAddToCollection": "Masukkan ke dalam koleksi", + "HeaderActivity": "Aktivitas", + "HeaderActiveRecordings": "Rekaman Aktif", + "HeaderActiveDevices": "Perangkat Aktif", + "HeaderAccessScheduleHelp": "Buat jadwal akses untuk membatasi akses ke jam-jam tertentu.", + "HeaderAccessSchedule": "Jadwal Akses", + "HardwareAccelerationWarning": "Mengaktifkan akselerasi perangkat keras dapat menyebabkan ketidakstabilan di beberapa lingkungan. Pastikan sistem operasi dan driver video Anda sudah diperbarui sepenuhnya. Jika Anda mengalami kesulitan memutar video setelah mengaktifkan ini, Anda harus mengubah pengaturan kembali ke Tidak Ada.", + "HDPrograms": "Program HD", + "EncoderPresetHelp": "Pilih nilai yang lebih cepat untuk meningkatkan kinerja, atau nilai yang lebih lambat untuk meningkatkan kualitas.", + "H264CrfHelp": "The Constant Rate Factor (CRF) adalah pengaturan kualitas default untuk pembuat enkode x264. Anda dapat mengatur nilai antara 0 dan 51, di mana nilai yang lebih rendah akan menghasilkan kualitas yang lebih baik (dengan mengorbankan ukuran file yang lebih tinggi). Nilai waras antara 18 dan 28. Default untuk x264 adalah 23, jadi Anda dapat menggunakan ini sebagai titik awal.", + "GuideProviderSelectListings": "Pilih Daftar", + "GuideProviderLogin": "Login", + "Guide": "Panduan", + "GuestStar": "Bintang tamu", + "GroupVersions": "Versi grup", + "GroupBySeries": "Kelompokkan menurut seri", + "Genre": "Aliran", + "General": "Umum", + "Fullscreen": "Layar penuh", + "Friday": "Jumat", + "FormatValue": "Format: {0}", + "FolderTypeUnset": "Konten Campuran", + "FolderTypeMusicVideos": "Video musik", + "FolderTypeMusic": "Musik", + "FolderTypeMovies": "Film", + "FolderTypeBooks": "Buku", + "Filters": "Filter", + "FileReadError": "Terjadi kesalahan saat membaca file.", + "FileReadCancelled": "Pembacaan file telah dibatalkan.", + "FileNotFound": "File tidak ditemukan.", + "File": "File", + "FetchingData": "Mengambil data tambahan", + "Features": "Fitur", + "Favorite": "Favorit", + "FastForward": "Maju cepat", + "FFmpegSavePathNotFound": "Kami tidak dapat menemukan FFmpeg menggunakan jalur yang Anda masukkan. FFprobe juga diperlukan dan harus ada di folder yang sama. Komponen ini biasanya digabungkan dalam unduhan yang sama. Harap periksa jalurnya dan coba lagi.", + "Extras": "Ekstra", + "ExtractChapterImagesHelp": "Mengekstrak gambar bab akan memungkinkan klien untuk menampilkan menu pemilihan adegan grafis. Prosesnya bisa lambat, membutuhkan banyak sumber daya, dan mungkin memerlukan beberapa gigabyte ruang. Ini berjalan saat video ditemukan, dan juga sebagai tugas yang dijadwalkan setiap malam. Jadwal dapat dikonfigurasi di area tugas terjadwal. Tidak disarankan untuk menjalankan tugas ini selama jam penggunaan puncak.", + "ExtraLarge": "Ekstra besar", + "ExitFullscreen": "Keluar dari layar penuh", + "EveryNDays": "Setiap {0} hari", + "ErrorSavingTvProvider": "Terjadi kesalahan saat menyimpan penyedia TV. Harap pastikan itu dapat diakses dan coba lagi.", + "ErrorPleaseSelectLineup": "Pilih barisan dan coba lagi. Jika tidak ada daftar yang tersedia, periksa apakah nama pengguna, kata sandi, dan kode pos Anda sudah benar.", + "ErrorStartHourGreaterThanEnd": "Waktu akhir harus lebih lama dari waktu mulai.", + "ErrorGettingTvLineups": "Terjadi kesalahan saat mengunduh daftar TV. Harap pastikan informasi Anda benar dan coba lagi.", + "ErrorAddingXmlTvFile": "Ada kesalahan saat mengakses file XMLTV. Harap pastikan file tersebut ada dan coba lagi.", + "ErrorAddingTunerDevice": "Terjadi kesalahan saat menambahkan perangkat tuner. Harap pastikan itu dapat diakses dan coba lagi.", + "ErrorAddingMediaPathToVirtualFolder": "Terjadi kesalahan saat menambahkan jalur media. Harap pastikan jalurnya valid dan Jellyfin memiliki akses ke lokasi itu.", + "ErrorAddingListingsToSchedulesDirect": "Terjadi kesalahan saat menambahkan daftar ke akun Jadwal Langsung Anda. Jadwal Langsung hanya mengizinkan jumlah daftar terbatas per akun. Anda mungkin perlu masuk ke situs web Jadwal Langsung dan menghapus daftar orang lain dari akun Anda sebelum melanjutkan.", + "Episodes": "Episodes", + "Episode": "Episode", + "EndsAtValue": "Berakhir pada {0}", + "Ended": "Berakhir", + "EnableDetailsBannerHelp": "Tampilkan gambar spanduk di bagian atas halaman detail item.", + "EnableDetailsBanner": "Detail Spanduk", + "EnableThemeVideosHelp": "Putar video tema di latar belakang saat menjelajahi pustaka.", + "EnableThemeSongsHelp": "Putar lagu tema di latar belakang saat menjelajahi pustaka.", + "EnableStreamLoopingHelp": "Aktifkan ini jika live streaming hanya berisi beberapa detik data dan perlu terus diminta. Mengaktifkan ini saat tidak diperlukan dapat menyebabkan masalah.", + "EnableStreamLooping": "Putar otomatis live streaming" } From 0ba71ebd4b9b05dc690b36bd33db1e34d3789cea Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2020 13:15:41 +0000 Subject: [PATCH 23/31] Bump date-fns from 2.15.0 to 2.16.0 Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.15.0 to 2.16.0. - [Release notes](https://github.com/date-fns/date-fns/releases) - [Changelog](https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md) - [Commits](https://github.com/date-fns/date-fns/compare/v2.15.0...v2.16.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 203be0749d..8e08e10952 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "blurhash": "^1.1.3", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "core-js": "^3.6.5", - "date-fns": "^2.15.0", + "date-fns": "^2.16.0", "epubjs": "^0.3.85", "fast-text-encoding": "^1.0.3", "flv.js": "^1.5.0", diff --git a/yarn.lock b/yarn.lock index 86c167aee6..cf709af8a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3218,10 +3218,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" - integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== +date-fns@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.0.tgz#d34f0f5f2fd498c984513042e8f7247ea86c4cb7" + integrity sha512-DWTRyfOA85sZ4IiXPHhiRIOs3fW5U6Msrp+gElXARa6EpoQTXPyHQmh7hr+ssw2nx9FtOQWnAMJKgL5vaJqILw== dateformat@^2.0.0: version "2.2.0" From cf70659deb5d5f75086e3e2f89b91a4678903bde Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2020 14:36:12 +0000 Subject: [PATCH 24/31] Bump webpack-stream from 5.2.1 to 6.0.0 Bumps [webpack-stream](https://github.com/shama/webpack-stream) from 5.2.1 to 6.0.0. - [Release notes](https://github.com/shama/webpack-stream/releases) - [Commits](https://github.com/shama/webpack-stream/compare/v5.2.1...v6.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8e08e10952..2bea3ad8e2 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "stylelint-order": "^4.1.0", "webpack": "^4.44.1", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^6.0.0" }, "dependencies": { "alameda": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index cf709af8a4..287380aef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10918,7 +10918,7 @@ supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -11939,17 +11939,17 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-stream@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-5.2.1.tgz#35c992161399fe8cad9c10d4a5c258f022629b39" - integrity sha512-WvyVU0K1/VB1NZ7JfsaemVdG0PXAQUqbjUNW4A58th4pULvKMQxG+y33HXTL02JvD56ko2Cub+E2NyPwrLBT/A== +webpack-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-6.0.0.tgz#0524b739a3c3a487362ee2e97522d0b8d366a510" + integrity sha512-bYv7apmGB1j0JBpa5Fgyky/1mWyzHOnUPXv+RmkgpK4FXPWCMBspgnYFmhE7Ly68JSp77eonFzm6WArWy4afpQ== dependencies: fancy-log "^1.3.3" lodash.clone "^4.3.2" lodash.some "^4.2.2" - memory-fs "^0.4.1" + memory-fs "^0.5.0" plugin-error "^1.0.1" - supports-color "^5.5.0" + supports-color "^7.1.0" through "^2.3.8" vinyl "^2.1.0" webpack "^4.26.1" From 26b09014d02795a6d12c53170020f0771e8ca850 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 29 Aug 2020 16:34:21 +0200 Subject: [PATCH 25/31] Fix bad global restriction --- src/components/serviceworker/notifications.js | 3 ++- src/scripts/apploader.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/serviceworker/notifications.js b/src/components/serviceworker/notifications.js index 339d521bbc..9b50553244 100644 --- a/src/components/serviceworker/notifications.js +++ b/src/components/serviceworker/notifications.js @@ -26,7 +26,8 @@ }); } - window.addEventListener('notificationclick', function (event) { + /* eslint-disable-next-line no-restricted-globals -- self is valid in a serviceworker environment */ + self.addEventListener('notificationclick', function (event) { var notification = event.notification; notification.close(); diff --git a/src/scripts/apploader.js b/src/scripts/apploader.js index f5f6850c40..fdf52fd699 100644 --- a/src/scripts/apploader.js +++ b/src/scripts/apploader.js @@ -35,7 +35,8 @@ // Promise() being missing on some legacy browser, and a funky one // is Promise() present but buggy on WebOS 2 window.Promise = undefined; - window.Promise = undefined; + /* eslint-disable-next-line no-restricted-globals -- Explicit check on self needed */ + self.Promise = undefined; } if (!window.Promise) { From 3a8e0394dccf0ebc23d5f46b2e09e484af134bd5 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 29 Aug 2020 16:44:36 +0200 Subject: [PATCH 26/31] Remove withCredentials from xhrSetup in hls.js --- src/plugins/htmlAudioPlayer/plugin.js | 5 +---- src/plugins/htmlVideoPlayer/plugin.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/plugins/htmlAudioPlayer/plugin.js b/src/plugins/htmlAudioPlayer/plugin.js index acce15df88..6f413fac50 100644 --- a/src/plugins/htmlAudioPlayer/plugin.js +++ b/src/plugins/htmlAudioPlayer/plugin.js @@ -132,10 +132,7 @@ class HtmlAudioPlayer { return new Promise(function (resolve, reject) { requireHlsPlayer(function () { const hls = new Hls({ - manifestLoadingTimeOut: 20000, - xhrSetup: function (xhr, url) { - xhr.withCredentials = true; - } + manifestLoadingTimeOut: 20000 }); hls.loadSource(val); hls.attachMedia(elem); diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 58c8624e34..88329fecff 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -393,10 +393,7 @@ function tryRemoveElement(elem) { return new Promise((resolve, reject) => { requireHlsPlayer(() => { const hls = new Hls({ - manifestLoadingTimeOut: 20000, - xhrSetup(xhr) { - xhr.withCredentials = true; - } + manifestLoadingTimeOut: 20000 }); hls.loadSource(url); hls.attachMedia(elem); From ac0fdd8059573045c954fcb26829f48ca4b40fbb Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 19:43:05 +0300 Subject: [PATCH 27/31] Fix fake image zoom --- src/components/slideshow/slideshow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index 82f541a116..6e2d5c379c 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -256,7 +256,7 @@ export default function (options) { /** * Handles zoom changes. */ - function onZoomChange(scale, imageEl, slideEl) { + function onZoomChange(swiper, scale, imageEl, slideEl) { const zoomImage = slideEl.querySelector('.swiper-zoom-fakeimg'); if (zoomImage) { From f915ecd400d680a71f2d66b3abb1de9910e3e72a Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 19:44:05 +0300 Subject: [PATCH 28/31] Add vendor styles polyfill --- src/legacy/vendorStyles.js | 32 ++++++++++++++++++++++++++++++++ src/scripts/site.js | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 src/legacy/vendorStyles.js diff --git a/src/legacy/vendorStyles.js b/src/legacy/vendorStyles.js new file mode 100644 index 0000000000..3b9dd9cf10 --- /dev/null +++ b/src/legacy/vendorStyles.js @@ -0,0 +1,32 @@ +// Polyfill for vendor prefixed style properties + +(function () { + const vendorProperties = { + 'transform': ['webkitTransform'], + 'transition': ['webkitTransition'] + }; + + const elem = document.createElement('div'); + + function polyfillProperty(name) { + if (!(name in elem.style)) { + for (const vendorName of vendorProperties[name] || []) { + if (vendorName in elem.style) { + console.debug(`polyfill '${name}' with '${vendorName}'`); + + Object.defineProperty(CSSStyleDeclaration.prototype, name, { + get: function () { return this[vendorName]; }, + set: function (val) { this[vendorName] = val; } + }); + + break; + } + } + } + } + + if (elem.style instanceof CSSStyleDeclaration) { + polyfillProperty('transform'); + polyfillProperty('transition'); + } +})(); diff --git a/src/scripts/site.js b/src/scripts/site.js index 0e1e44251c..fda46b8dbe 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -222,6 +222,7 @@ function initClient() { }); require(['mouseManager']); require(['focusPreventScroll']); + require(['vendorStyles']); require(['autoFocuser'], function(autoFocuser) { autoFocuser.enable(); }); @@ -655,6 +656,7 @@ function initClient() { }); define('slideshow', [componentsPath + '/slideshow/slideshow'], returnFirstDependency); define('focusPreventScroll', ['legacy/focusPreventScroll'], returnFirstDependency); + define('vendorStyles', ['legacy/vendorStyles'], returnFirstDependency); define('userdataButtons', [componentsPath + '/userdatabuttons/userdatabuttons'], returnFirstDependency); define('listView', [componentsPath + '/listview/listview'], returnFirstDependency); define('indicators', [componentsPath + '/indicators/indicators'], returnFirstDependency); From fad3b562ea3dc1fc9a11ce41039b9efa44bb0981 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 20:10:23 +0300 Subject: [PATCH 29/31] Fix variable usage (SonarCloud) --- src/legacy/vendorStyles.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/legacy/vendorStyles.js b/src/legacy/vendorStyles.js index 3b9dd9cf10..74a90ba04d 100644 --- a/src/legacy/vendorStyles.js +++ b/src/legacy/vendorStyles.js @@ -10,7 +10,7 @@ function polyfillProperty(name) { if (!(name in elem.style)) { - for (const vendorName of vendorProperties[name] || []) { + (vendorProperties[name] || []).every((vendorName) => { if (vendorName in elem.style) { console.debug(`polyfill '${name}' with '${vendorName}'`); @@ -19,9 +19,11 @@ set: function (val) { this[vendorName] = val; } }); - break; + return false; } - } + + return true; + }); } } From 3aa8304efc0c05115e5224d864344e1f5eb7b860 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2020 12:41:59 +0000 Subject: [PATCH 30/31] Bump html-webpack-plugin from 4.3.0 to 4.4.0 Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/jantimon/html-webpack-plugin/releases) - [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v4.3.0...v4.4.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 39c7937bd7..6dfe8fe5a1 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "gulp-sass": "^4.0.2", "gulp-sourcemaps": "^2.6.5", "gulp-terser": "^1.4.0", - "html-webpack-plugin": "^4.3.0", + "html-webpack-plugin": "^4.4.0", "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index d3aee5fe79..3e61547551 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5544,10 +5544,10 @@ html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== -html-webpack-plugin@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd" - integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w== +html-webpack-plugin@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.4.0.tgz#ed6ab2b5e4e476ffa3c5ce52505aa31a42075029" + integrity sha512-FHeg2JN9ar1kaR0SLgbF07w46o/n1nGszyByYlPxqEymSpl82vA8EX0leE67kZr3GJnOBh8BbBzmCLO6O1YTIQ== dependencies: "@types/html-minifier-terser" "^5.0.0" "@types/tapable" "^1.0.5" From 5ca7342e3eb27bc90632eba6f95af53cf2bf18f0 Mon Sep 17 00:00:00 2001 From: millallo Date: Sun, 30 Aug 2020 10:41:51 +0000 Subject: [PATCH 31/31] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/ --- src/strings/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/it.json b/src/strings/it.json index f89c8b3a65..df11607245 100644 --- a/src/strings/it.json +++ b/src/strings/it.json @@ -292,7 +292,7 @@ "HeaderInstantMix": "Mix Istantaneo", "HeaderKeepRecording": "Mantieni la registrazione", "HeaderKeepSeries": "Mantieni Serie TV", - "HeaderKodiMetadataHelp": "Jellyfin include il supporto nativo per i file metadati NFO. Per attivare o disattivare i metadati NFO, utilizzare la scheda Metadati per configurare le opzioni per i tipi di supporto.", + "HeaderKodiMetadataHelp": "Per abilitare o disabilitare i metadati NFO, editare la libreria e configurare l'opzione nella sezione Metadati.", "HeaderLatestEpisodes": "Ultimi Episodi Aggiunti", "HeaderLatestMedia": "Ultimi Media", "HeaderLatestMovies": "Ultimi Film Aggiunti",